前置知识

Any 任意类型

定义为 any 类型是可以赋值给任意类型,当类型转换遇到困难或者数据结构复杂难以定义可以使用 any。any 给了我们很大的自由,如果所有类型定义为any,TypeScript 其实就失去的它存在的意义了。

any 类型太宽松了。很多场景会编写出类型正确编译通过,运行时报错的代码。例如

  1. let cc: any;
  2. cc.trim();
  3. new cc();
  4. cc.cc.eat(); // 完全可以通过编译,但是运行明显是会报错

Unknown 类型

在 TypeScript 中,当我们不确定一个类型是什么类型,可以选择给其声明为 any 或者 unknown。但实际上,TypeScript 更推荐使用 unknown, 因为unknown可以保证类型安全。如果使用 any ,其实是放弃了类型检查。

  1. let cc: unknown;
  2. cc = true; // ok
  3. cc = 'yes'; // ok
  4. cc = 100; // ok

我们发现类型 unknown 和 any 。对它进行任何类型的赋值都是正确的。 但是我们尝试将 unknown 赋值给其他类型值会怎么样呢?

  1. let cc: unknown;
  2. let a: unknown = cc; // ok
  3. let b: any = cc; // ok
  4. let c: string = cc; // no ok
  5. let d: number = cc; // no ok
  6. // ...

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。

  1. let cc: unknown;
  2. cc.trim(); // no ok
  3. new cc(); // no ok
  4. cc.cc.eat(); // no ok

对比后我们知道,any 可以随意取值赋值操作,并不会进行任何类型的检查。但是 unknown 就不一样了,它需要先进行断言或者类型守卫后才能通过编译

  1. let str: unknown;
  2. // str.length // 直接取 .length 肯定不ok 。但是 any 可以
  3. // 如果要取 length, 我们可以将 str 断言成 string, 或者 intanceof 判断为 String 类型
  4. if (str instanceof String) {
  5. console.log(str.length) // ok
  6. }
  7. // console.log((str as string).length) // ok

这下我们知道 unknown 为什么是安全的了。它只是指明了类型还未确认,后续还需要去断言,也就是它并未放弃类型检查。

类型推论

有时候没有明确指明类型,但是 TypeScript 还是能帮我们推断出一个类型

  1. let count = 100 // ts 推断出 count 是一个 number 类型
  2. count = '100' // 编译报错

有一点要特别注意的是,当在声明变量的时候没有给变量赋值时,该变量会被推断成 any 类型。从而跳过类型检查。

  1. let x
  2. x = 1
  3. x = '1'
  4. // 这段代码完全 ok 的。 编译时不会报错

联合类型

联合类型表示取值可以为多种类型中的某一种。

  1. let money: number | string
  2. money = 10000
  3. money = '1w'

当 TypeScript 还不知道联合类型变量到底是哪一种变量时候,那么我们只能访问该联合类型共有的属性和方法。

  1. function getMoney(money: string | number) {
  2. console.log(money.length) // 编译报错,因为 number 没有length 属性
  3. console.log(money.toString()) // 编译成功,因为 toString 是 number 和 string 的共有方法
  4. }

类型断言

  • 类型断言可以将一个联合类型的变量,指定为一个更加具体的类型
  • 不能将联合类型断言成不存在的类型,当然除了 any

有时候你会比 TypeScript 更了解某个值的详细信息。通常发生在你会更新清楚地知道一个比它现在更确切的类型。需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误。

断言的两种写法

  • 值 as 类型
  • <类型>值 (在React开发中不采用这种方式,因为在React中<>会被认为是一个ReactElement)
    1. function getMoney(money: string | number) {
    2. console.log((money as number).toFixed(2))
    3. console.log((money as string).length)
    4. console.log((money as boolean)) // 断言成不存在的值, no ok
    5. console.log((money as any as boolean)) // 通过 双重断言 欺骗编译器
    6. console.log((money as any))
    7. }

    一方面不能滥用 as any,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡(这也是 TypeScript 的设计理念之一),才能发挥出 TypeScript 最大的价值。

类型别名

类型别名用来给一个类型起个新名字。 关键词 type ,常用于联合类型。

  1. type Msg = string | string []
  2. function toast(msg: Msg) {
  3. // ...
  4. }

我们类型别名和接口很像,那有什么差异呢?官网说:

Because an ideal property of software is being open to extension, you should always use an interface over a type alias if possible.(因为完美的软件是对拓展开放,所以只要可能,你通常更需要使用接口而不是别名类型。)
If you can’t express some shape with an interface and you need to use a union or tuple type, type aliases are usually the way to go.(如果你无法用接口表示类型,同时你需要使用联合类型或元组,那么别名类型会是经常使用的方式。)

小结一下:

  • 优先使用接口
  • 如果需要联合类型或者元组等类型处理,则可以使用别名类型声明
  • 当不涉及接口继承(extends)、类实现(implement)时,接口和别名只作为类型定义时,两者皆可。

泛型正文

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:

  • 类的实例成员
  • 类的方法
  • 函数参数
  • 函数返回值

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

泛型函数

需求:创建一个长度为 length 的数组,数组里面的值用 value 来填充

  1. function createArray(length: number, value: any): any[] {
  2. const res = []
  3. for(let i = 0; i < length; i++) {
  4. res.push(value) // i
  5. }
  6. return res
  7. }
  8. const res = createArray(5, 'x')
  9. console.log(res)

思考:以上代码可以编译通过,但是我们发现返回的是一个 any 类型的数组。我们完全可以在函数内部改变修改返回数组的类型。 是不是就缺乏一些约束,或者是对应关系。例如:当我们传入一个 number 类型,我们知道任何类型的数组都有可能被返回。。。我们更希望返回数组的类型应该和传入 value 的类型保持一致。

这个时候「泛型」就派上用场了。。。

  1. function createArray<T>(length: number, value: T): T[] {
  2. const res: T[] = []
  3. for(let i = 0; i < length; i++) {
  4. res.push(value)
  5. }
  6. return res
  7. }
  8. const res = createArray(5, 'x');
  9. console.log(res)

观察以上泛型代码:
1、我们可以看到如何定义一个泛型。在函数名后面添加一个 ,并在参数、函数返回值、函数内部可以使用到 这个 T 。 so… 可以理解为 T 作用域只限于函数内部使用
2、T 可以理解为类型的形参,我们会在使用的是传递一个具体的类型
3、当然,T 并不是固定了,当然你可以 ABC… 任由你喜欢。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。 下面我们介绍一下一些常见泛型变量代表的意思:

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

4、说好的使用的时候在传递类型,但以上代码并没有看到主动传递类型。因为类型推论会自动推算出来

多个类型参数

在使用泛型的时候,我们可以一次性定义多个类型参数。
需求:我们想定义一个 swap 函数,用来交换输入的元组。

  1. function swap<A, B>(tuple: [A, B]): [B, A] {
  2. return [tuple[1], tuple[0]]
  3. }
  4. console.log(swap([1, '2']))

泛型类

需求:创建一个类,实例方法 push 可以添加值,实例方法会 getRandom 会随机一个已添加过的值

  1. class MyArray<T> {
  2. private arr: T[] = []
  3. // ...
  4. push(value: T) {
  5. this.arr.push(value)
  6. }
  7. getRandom(): T {
  8. return this.arr[Math.floor(Math.random() * this.arr.length)]
  9. }
  10. }
  11. const my = new MyArray();
  12. my.push(1);
  13. my.push(2);
  14. my.push(99);
  15. console.log(my.getRandom())

观察以上代码:
当创建实例的时候并没有主动传入 类型。 此时的 T 是啥类型???

泛型接口

来看一个例子:

  1. interface Calc {
  2. <T>(a: T, b: T): T
  3. }
  4. const sum: Calc = function<T>(a: T, b: T): T {
  5. return a
  6. }
  7. sum<number>(1, 2);

思考:返回的是单个 a, 如果返回的是 a 和 b 的运算结果呢? 运算符“+”不能应用于类型“T”和“T” . 泛型 T 可以是任意类型,因为不知道会传什么东西,所以不能相加。 那咋整?

在来看一个几乎类似的例子:我只是将类型形参 T 提升到了接口上面定义。

  1. interface Calc<T> {
  2. (a: T, b: T): T
  3. }
  4. const sum: Calc<number> = function(a: number, b: number): number {
  5. return a + b;
  6. }
  7. sum(1, 2)

思考:泛型定义在 接口上 和 定义在接口里的函数上 有什么区别呢?
定义在接口上时,我们在使用接口的时候就必须先明确传递一个类型。
定义在接口里的函数时,我们在定义「形状」为该接口的函数时,继续保留 泛型,直到调用该函数时在明确传递类型。

默认泛型

我们之前把泛型理解为类型的形参,当然默认泛型,自然也可以理解为默认参数了

  1. function createArray<T = string>(length: number, value: T): T[] {
  2. const res: T[] = []
  3. for(let i = 0; i < length; i++) {
  4. res.push(value)
  5. }
  6. return res
  7. }
  8. const res = createArray(5, 1);

观察以上代码:
是不是有疑惑,默认泛型为 string,创建时候确实没传递具体类型,是不是应该走 string,但传入一个 1 为 number 类型编译却可以通过。

其实最开始的时候我们说过不传递也 ts 也会根据类型推导,知道传递的是什么类型。当可以推导出来类型时,推导 > 默认。

看看另外一个例子,也是之前的例子,泛型类

  1. class MyArray<T = string> {
  2. private arr: T[] = []
  3. // ...
  4. push(value: T) {
  5. this.arr.push(value)
  6. }
  7. getRandom(): T {
  8. return this.arr[Math.floor(Math.random() * this.arr.length)]
  9. }
  10. }
  11. const my = new MyArray(); // 此处没有传递类型,也推导不出具体类型,走了默认类型 string
  12. my.push(1); // 编译报错
  13. my.push(2);
  14. my.push(99);
  15. console.log(my.getRandom())

或者

  1. interface T1<T> {}
  2. type T2 = T1; // 报错 泛型类型“T1<T>”需要 1 个类型参数。
  3. // 方案一:
  4. type T2 = T1<string>
  5. // 方案二:
  6. interface T1<T = string> {}

泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

  1. function logger<T>(arg: T): T {
  2. console.log(arg.length); // 并不是任何类型都有 length 属性,所以编译报错
  3. return arg;
  4. }

我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

  1. interface Lengthwise {
  2. length: number;
  3. }
  4. function logger<T extends Lengthwise>(arg: T): T {
  5. console.log(arg.length);
  6. return arg;
  7. }
  8. logger(1) // 虽然以上的定义编译是没问题的,但是在调用logger时,也要保证传入值有 length 属性

保证有length 属性??? 那我。。。

  1. const obj = {
  2. length: 6
  3. }
  4. type hasLengthObj = typeof obj // 得到一份包含length形状的类型
  5. logger<hasLengthObj>('1')

so。。。 可以理解为类型有一个 length 属性就行 ok 。 不关心你是以什么方式去得到的。

再看 T extends Lengthwise。 T 继承 Lengthwise,T 是 Lengthwise 的子类型,T 需要满足 Lengthwise 的约束条件。可以理解为 T 所拥有的形状只准多,不准少于 Lengthwise 的形状。

  1. interface Lengthwise {
  2. length: number;
  3. }
  4. const obj = {
  5. length: 6,
  6. age: 6
  7. }
  8. type hasLengthObj = typeof obj
  9. let l1: Lengthwise = { length: 2 };
  10. let l2: hasLengthObj = { length: 6, age: 6 };
  11. l1 = l2;
  12. // l2 = l1;

泛型工具类型

typeof

在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。

  1. interface Person {
  2. name: string;
  3. age: number;
  4. }
  5. const man: Person = {
  6. name: 'cc',
  7. age: 10
  8. }
  9. type Human = typeof man // -> Person
  10. const women: Human = {
  11. name: 'xx',
  12. age: 9
  13. }
  1. function toArray(x: number): Array<number> {
  2. return [x];
  3. }
  4. type Func = typeof toArray; // -> (x: number) => number[]

typeof 我的理解是 可以 copy 一份完整的类型格式(或者说形状)。

keyof

keyof 操作符可以用来一个对象中的所有 key 值:

  1. interface Person {
  2. name: string;
  3. age: number;
  4. }
  5. type K1 = keyof Person;
  6. type K2 = keyof Person[];
  7. type K3 = keyof { [x: string]: Person }; // 索引签名参数类型必须为 "string" 或 "number"。

in

in 用来遍历枚举类型:

  1. type Keys = "a" | "b" | "c"
  2. type Obj = {
  3. [p in Keys]: any
  4. }

Partial

Partial 的作用就是将某个类型里的属性全部变为可选项 ?。

  1. type Partial<T> = { [P in keyof T]?: T[P] };
  2. interface A {
  3. a1: string;
  4. a2: number;
  5. a3: boolean;
  6. }
  7. type aPartial = Partial<A>;
  8. const a: aPartial = {}; // 不会报错

首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

Readonly

  1. type Readonly<T> = { readonly [P in keyof T]: T[P] };

….Partial、Required、Readonly、Record 和 ReturnType