掘金 阅读 ( ) • 2023-02-01 00:14

theme: channing-cyan

这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天

TypeScript 介绍

  1. TypeScript 是 JavaScript 的超集,提供了 JavaScript 的所有功能,并提供了可选的静态类型、Mixin、类、接口和泛型等特性。
  2. TypeScript 的目标是通过其类型系统帮助及早发现错误并提高 JavaScript 开发效率。
  3. 通过 TypeScript 编译器或 Babel 转码器转译为 JavaScript 代码,可运行在任何浏览器,任何操作系统。
  4. 任何现有的 JavaScript 程序都可以运行在 TypeScript 环境中,并只对其中的 TypeScript 代码进行编译。
  5. 在完整保留 JavaScript 运行时行为的基础上,通过引入静态类型定义来提高代码的可维护性,减少可能出现的 bug。
  6. 永远不会改变 JavaScript 代码的运行时行为,例如数字除以零等于 Infinity。这意味着,如果将代码从 JavaScript 迁移到 TypeScript ,即使 TypeScript 认为代码有类型错误,也可以保证以相同的方式运行。
  7. 对 JavaScript 类型进行了扩展,增加了例如 anyunknownnevervoid
  8. 一旦 TypeScript 的编译器完成了检查代码的工作,它就会 擦除 类型以生成最终的“已编译”代码。这意味着一旦代码被编译,生成的普通 JS 代码便没有类型信息。这也意味着 TypeScript 绝不会根据它推断的类型更改程序的 行为。最重要的是,尽管可能会在编译过程中看到类型错误,但类型系统自身与程序如何运行无关。
  9. 在较大型的项目中,可以在单独的文件 tsconfig.json 中声明 TypeScript 编译器的配置,并细化地调整其工作方式、严格程度、以及将编译后的文件存储在何处。

泛型

泛型是一种捕获参数类型的方法,用来创建能够在多种类型上工作可重用的组件,而不是单个类型,这样用户就可以以自己的数据类型来使用组件。

function identity<T>(arg: T): T {
  return arg;
}

这里,我们使用了一个类型变量 T,它是一种特殊的变量,只用于表示类型而不是值。T 帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。我们再次使用了 T 当做返回值类型,这样参数类型与返回值类型就是相同的了。

我们可以用两种方式调用一个泛型函数:

  1. 第一种方式是将所有参数(包括类型参数)传递给函数。
let output = identity<string>("myString");
// let output: string

这里,我们显式地将 T 设置为 string,使用了 <> 括起来,作为函数调用的参数之一。

  1. 第二种方式是最常见的,使用类型参数推断,编译器根据传入的参数类型自动为我们设置 T 的类型。
let output2 = identity("myString");
// let output2: string

注意,我们不必在尖括号(<>)中显式传递类型;编译器只是根据值 myString,将 T 设置为其类型。虽然类型参数推断是保持代码更短、更可读的有用工具,但当编译器无法推断类型时,需要像第一种方式那样显式传递类型参数,这在更复杂的示例中可能会发生。

泛型变量

如果我们要在一个函数中,打印一个参数的长度。

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length); // Property 'length' does not exist on type 'Type'.
  return arg;
}

泛型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 length 属性的。

但如果我们操作的是 T 类型的数组而不直接是 Tlength 属性是存在的。

function loggingIdentity<Type>(arg: Type[]): Type[] {
  console.log(arg.length);
  return arg;
}

泛型函数 loggingIdentity 接收类型参数 T 和参数 arg,它是个元素类型是 T 的数组,并返回元素类型是 T 的数组。

泛型类型

  1. 可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。
let myIdentity: <Input>(arg: Input) => Input = identity;
  1. 还可以使用带有调用签名的对象字面量类型来定义泛型函数。
let myIdentity2: { <Type>(arg: Type): Type } = identity;
  1. 可以把上面例子里的对象字面量拿出来做为一个泛型接口。
interface GenericIdentityFn {
  <Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
  return arg;
}
let myIdentity: GenericIdentityFn = identity;
  1. 可以把泛型参数也当作整个接口的一个参数,就能清楚的知道使用的具体是哪个泛型类型,接口里的其它成员也能知道这个参数的类型了。
interface GenericIdentityFn<Type> {
  (arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
  return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
let myIdentity2: GenericIdentityFn<string> = identity;

我们现在有了一个非泛型函数签名,它是泛型类型的一部分,而不是描述泛型函数。当我们使用 GenericIdentityFn 时,我们现在还需要指定相应的类型参数(这里:number),从而有效地锁定了之后代码里使用的类型。了解何时将类型参数直接放在调用签名上和接口本身上,将有助于描述类型的哪些方面是属于泛型的。

除了泛型接口,我们还可以创建泛型类。但是,无法创建泛型枚举和泛型命名空间。

泛型类

泛型类与泛型接口相似,在类名称后面的尖括号(<>)中有一个泛型类型参数列表。

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

与接口一样,将类型参数放在类本身可以确保类的所有成员都使用同一类型。我们知道,类有两部分:静态部分和实例部分。泛型类指的是实例部分的类型,所以类的静态成员不能使用类的泛型类型。

泛型约束

我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在 loggingIdentity 例子中,我们想访问 arglength 属性,但是编译器并不能证明每种类型都有 length 属性,所以就报错了。我们希望只要该类型具有此成员,我们就允许使用它。

我们需要创建一个包含 .length 属性的接口,使用这个接口和 extends 关键字来实现约束:

interface Lengthwise {
  length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length);
  return arg;
}

由于泛型函数现在受到约束,它将不再适用于任何类型:

loggingIdentity(3); // Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.

我们需要传入符合约束类型的值,必须包含所有必需的属性:

loggingIdentity({length: 10, value: 3});

在泛型约束中使用类型参数

可以声明一个类型参数,且它被另一个类型参数所约束。比如,现在我们想要用属性名从对象里获取这个属性。并且我们想要确保这个属性存在于对象 obj 上,因此我们需要在这两个类型之间使用约束。

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m"); // Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

在泛型里使用类类型

使用泛型创建工厂函数时,需要通过其构造函数引用类类型。

function create<Type>(c: { new (): Type }): Type {
  return new c();
}

更高级的示例使用原型属性来推断并约束构造函数与类实例的关系,Mixins 设计使用了此模式。

class BeeKeeper {
  hasMask: boolean = true;
}
class ZooKeeper {
  nametag: string = "Mikle";
}
class Animal {
  numLegs: number = 4;
}
class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

类型转换(Casting)

在处理类型时,有时需要重写变量的类型,例如库提供了不正确的类型。强制转换就是重写类型的过程。

as

转换变量的一种简单方法是使用 as 关键字,这将直接更改给定变量的类型。

let x: unknown = 'hello';
console.log((x as string).length);

强制转换实际上不会改变变量内数据的类型,例如,以下代码没有按预期工作,因为变量 x 仍然是一个数字。

let x: unknown = 4;
console.log((x as string).length); // prints undefined since numbers don't have a length

TypeScript 仍然会尝试对类型转换进行类型检查,以防止看起来不正确的类型转换,例如,由于 TypeScript 知道在不转换数据的情况下将字符串转换为数字是没有意义的,因此下面将抛出类型错误:

console.log((4 as string).length); // Error: Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

<>

使用 <> 的工作原理与使用 as 转换相同。

let x: unknown = 'hello';
console.log((<string>x).length);

这种类型的转换不适用于 TSX,例如在处理 React 文件时。

强制转换

若要覆盖 TypeScript 在强制转换时可能引发的类型错误,先强制转换为 unknown 类型,然后再转换为目标类型。

let x = 'hello';
console.log(((x as unknown) as number).length); // x is not actually a number so this will return undefined

类型守卫

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

interface IBird {
  fly();
  layEggs();
}
interface IFish {
  swim();
  layEggs();
}
class Bird implements IBird {
  fly(){}
  layEggs(){}
}
class Fish implements IFish {
  swim(){}
  layEggs(){}
}
function getSmallPet(): IFish | IBird {
  return 1 ? new Fish() : new Bird();
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

这个例子里, Bird 具有一个 fly 成员。 我们不能确定一个 Bird | Fish 类型的变量是否有 fly 方法。如果变量在运行时是 Fish 类型,那么调用 pet.fly() 就出错了。

联合类型适合于那些值可以为不同类型的情况,我们只能访问联合类型中共同拥有的成员。但当我们想确切地了解是否为 Fish 时怎么办?JavaScript 里常用来区分 2 个可能值的方法是检查成员是否存在。

// 每一个成员访问都会报错
if (pet.swim) {
  pet.swim();
} else if (pet.fly) {
  pet.fly();
}

为了让这段代码工作,我们需要使用类型断言:

if ((<Fish>pet).swim) {
  (<Fish>pet).swim();
}
else {
  (<Bird>pet).fly();
}

自定义的类型保护

类型守卫就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。可以看到,上面我们不得不多次使用类型断言,而通过类型守卫机制,我们一旦检查过类型,就能在之后的每个分支里清楚地知道 pet 的类型了。要定义一个类型守卫,我们只要简单地定义一个函数,它的返回值是一个类型谓词

function isFish(pet: Fish | Bird): pet is Fish {
  return (<Fish>pet).swim !== undefined;
}

类型谓词为 parameterName is Type 形式(例如 pet is Fish), parameterName 必须是来自于当前函数签名里的一个参数名。每当使用一些变量调用 isFish 时,TypeScript 会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

// 'swim' 和 'fly' 调用都没有问题了
if (isFish(pet)) {
  pet.swim();
}
else {
  pet.fly();
}

TypeScript 不仅知道在 if 分支里 petFish 类型; 它还清楚在 else 分支里,一定 不是 Fish 类型,一定是 Bird 类型。

typeof 类型守卫

如果像下面这样利用类型断言来写:

function isNumber(x: any): x is number {
  return typeof x === "number";
}
function isString(x: any): x is string {
  return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
  if (isNumber(padding)) {
    return Array(padding + 1).join(" ") + value;
  }
  if (isString(padding)) {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

这里定义了每个函数来判断类型是否是对应原始类型,这太麻烦了。幸运的是,现在我们不必将 typeof x === "number" 抽象成一个函数,因为 TypeScript 可以将它识别为一个类型守卫。也就是说我们可以直接在代码里检查类型了。

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 类型守卫只有两种形式能被识别:typeof v === "typename"typeof v !== "typename"typename 必须是 numberstringbooleansymbol。但是,TypeScript 不会阻止你与其它字符串比较,只是不会把那些表达式识别为类型守卫。

instanceof 类型守卫

instanceof 类型守卫是通过构造函数来缩小类型的一种方式。instanceof 的右侧要求是一个构造函数,TypeScript 将按照顺序缩小为:

  1. 如果它的类型不为 any,构造函数的 prototype 类型。
  2. 构造函数返回的类型联合。
interface Padder {
  getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) { }
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}
class StringPadder implements Padder {
  constructor(private value: string) { }
  getPaddingString() {
    return this.value;
  }
}
function getRandomPadder() {
  return Math.random() > 0.5 ? new SpaceRepeatingPadder(4) : new StringPadder("  ");
}

// 类型为SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
  padder; // 类型缩小为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
  padder; // 类型缩小为'StringPadder'
}

空值类型守卫

使用类型保护来去除联合类型中的 null 与 JavaScript 代码一致。

function f(sn: string | null): string {
  if (sn == null) {
    return "default";
  }
  else {
    return sn;
  }
}

也可以使用短路运算符:

function f2(sn: string | null): string {
  return sn || "default";
}

在编译器无法消除 nullundefined 的情况下,可以使用类型断言运算符手动删除它们。语法是后缀添加 !identifier!,从标识符的类型中删除 nullundefined

interface UserAccount {
  id: number;
  email?: string;
}
function getUser(id: string): UserAccount | undefined {
  return { email: '' } as any;
}
const user = getUser("admin");
user.id; // Object is possibly 'undefined'.
if (user) {
  user.email.length; // Object is possibly 'undefined'.
}
// 如果确定这些对象或字段存在,则添加短路可空性
user!.email!.length;