Skip to content

TS 学习系列 02 ---TypeScript 数据类型 ​

TypeScipt 中为了使编写的代码更加规范,更有利于维护,增加了类型校验,所有以后写 ts 代码必须要指定类型 在 js 后面可以不指定类型,但是后期不能改变其类型,不然会报错,但是只会报错,不会阻止代码编译,因为 JS 是可以允许的

一、基础类型 ​

1. boolean 类型 ​

ts
let isDone: boolean = false;
// let isDone = false; // 这样也可以

isDone = true; // 正确写法, boolean只允许true false两个值
isDone = 234; // 报错 Type 'number' is not assignable to type 'boolean'

2. number 类型 ​

和 js 一样,ts 里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,ts 还支持 ECMAScript 2015(ES6)中引入的二进制和八进制字面量。

ts
let int: number = 6; // 十进制
let hex: number = 0xf00d; // 十六进制
let binary: number = 0b1010; // 二进制
let octal: number = 0o744; // 八进制

3. string 类型 ​

和 js 一样,可以使用双引号( ")或单引号(')表示字符串,也可以使用模板字符串(`)

ts
let str:string = 'hello ts'; // 单引号
let str1:string = "刘德华"; // 双引号
let names:string = `你的偶像是${str1}吗?`; // ES6模板字符串
console.log(names);

4. Array 类型 ​

ts 有四种方式可以定义数组。

ts
//第一种定义数组方法
let arr1: number[] = [1, 2, 3]; // 一个全是数字的数组

//第二种定义数组方法
let arr2: Array<string> = ["1", "周杰伦", "馒头"]; // 一个全是字符串的数组

//第三种定义数组方法(用了下面的元组类型)
let arr3: [number, string] = [123, "string"];

//第四种定义数组方法(用了下面的任意类型)
let arr4: any[] = [1, "string", true, null];

4.1 数组解构 ​

ts
let x: number;
let y: number;
let z: number;
let arr = [0, 1, 2, 3, 4];
[x, y, z] = arr;

4.2 数组展开运算符 ​

ts
let arr = [0, 1];
let arr2 = [...arr, 2, 3, 4];

4.3 数组遍历 ​

ts
let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
  console.log(i);
}

5. ReadonlyArray 类型(只读数组) ​

只读数组中的数组成员和数组本身的长度等属性都不能够修改,并且也不能赋值给原赋值的数组 其实只读数组只是一个内置定义的泛型接口

ts
let array1: number[] = [1, 2, 3, 4];
let readArr: ReadonlyArray<number> = array1;
readArr[1] = 5; //报错 Index signature in type 'readonly number[]' only permits reading.
readArr.length = 5; //报错 Cannot assign to 'length' because it is a read-only property
array1 = readArr; //报错,因为readArr的类型为Readonly,已经改变了类型 The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
array1 = readArr as number[]; // 正确,不能通过上面的方法复制,但是可以通过类型断言的方式,类型断言见下面
console.log(readArr);

6. 元组 Tuple ​

元组是 TS 中特有的类型,工作方式类似于数组。允许表示一个已知元素数量和类型的数组,各元素的类型不必相同,值需与声明的类型一一对应。

ts
// 1. 定义一对值分别为 string和number类型的元组
let a: [number, string];
a = [1, "hi"]; // 正确
// a = ['hi',23] // 报错 值须于类型一一对应

a[0]; // 1
a[1]; // 'hi'

与数组一样,元组也可以使用 readonly 修辞了,但是,尽管出现了 readonly 类型修饰符,但类型修饰符只能用于数组类型和元组类型的语法

ts
let err1: readonly Set<number>; // 错误!
let err2: readonly Array<boolean>; // 错误!

let okay: readonly boolean[]; // works fine

7. enum 类型(枚举) ​

enum 类型是对 JS 标准数据类型的一个补充。 像 C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

7.1 数字枚举 ​

ts
enum Color {
  red,
  green,
  pink,
}
let c: Color = Color.pink;
console.log(c); // 2

注意:

  • 默认情况下,从 0 开始为元素编号。 当然也可以手动的指定成员的数值
  • 例如:上面的例子改成从 1 开始编号
ts
enum Color {
  red = 1,
  green,
  pink,
}
let c: Color = Color.pink;
console.log(c); // 3

全部都采用手动赋值

ts
enum Color {
  red = 14,
  green = 43,
  pink = 10,
}
let c: Color = Color.pink;
console.log(c); // 10

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。

例如,我们知道数值为 2,但是不确定它映射到 Color 里的哪个名字,我们可以查找相应的名字

ts
enum Color {
  Red = 1,
  Green,
  Blue,
}
let colorName: string = Color[2];

console.log(colorName); // 输出'Green'  因为上面代码里它的值是2

7.2 字符串枚举 ​

在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

ts
enum author {
  B1 = "张三",
  B2 = "李四",
  B3 = "王五",
  B4 = "赵六",
}
let auth: author = author.B2;
console.log(auth); // 李四

注意

  • 字符串枚举不能向数值枚举一样,通过枚举值来映射到枚举名称。例如 author['李四'] 是错误的
  • 字符串枚举没有初始值
  • 字符串枚举必须列出每一个枚举的值,否则会报错

7.3 常量枚举 ​

除了数字枚举和字符串枚举之外,还有一种特殊的枚举 --- 常量枚举。它是使用 const 关键字修饰的枚举,常量枚举会使用内联语法,不会为枚举类型编译生成任何 JavaScript;

例如,下面是数值枚举和常量枚举编译为 js 前后的对比:可以看到常量枚举编译后很简单。

image.png

7.4 异构枚举 ​

异构枚举的成员值是数字和字符串的混合

image.png

观察编译后的 JS 代码,可以发现数字枚举相对字符串枚举多了 “反向映射”,那么就可以通过枚举值找到枚举名称,例如

ts
console.log(Enum.A); //输出:0
console.log(Enum[0]); // 输出:A

8. Symbol 类型 ​

ts
const sym = Symbol();
let obj = {
  [sym]: "semlinker",
};

console.log(obj[sym]); // semlinker

9. any 类型(任意类型) ​

任意类型的数据就像 JS 一样,可以随意改变其类型。在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。

ts
let num: any = 123;
num = "string";
console.log(num); //string

使用场景:

ts
//如果在ts中想要获取DOM节点,就需要使用任意类型
let oDiv: any = document.getElementsByTagName("div")[0];
oDiv.style.backgroundColor = "red";
//按道理说DOM节点应该是个对象,但是TypeScript中没有对象的基本类型,所以必须使用any类型才不会报错

any 类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查.

缺点:

  • 若将 any 类型的值赋给其他已知类型的变量,则会将其他变量的类型改为 any,是很危险的

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

10. unknown 类型 ​

TypeScript 3.0 引入了新的 unknown 类型,它是 any 类型对应的安全类型。 就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是上面的 any)

unknown 和 any 的主要区别是 unknown 类型会更加严格: 在对 unknown 类型的值执行大多数操作之前,我们必须进行某种形式的检查。 而在对 any 类型的值执行操作之前,我们不必进行任何检查**

例如:

  • unknown 可以被赋值为任意类型
ts
let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK
  • 但是 unknown 类型只能被赋值给 any 类型和 unknown 类型本身,如果没有类型细化的话是不允许的
ts
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值,毕竟我们不知道变量 value 中存储了什么类型的值

若要将 unknown 类型的值赋给一个已知类型的变量,有如下方法:

  • 1、赋值前先判断值类型
ts
let a: string = "100";
let b: unknow = "hello";
if (typeof b === string) {
  a = b;
}
  • 2、类型断言

    类型断言: 可以告诉编译器变量的实际类型(断言在下文会细说)

ts
let a: string = "100";
let b: unknow = "hello";
a = b as string; // 类型断言
// 或者
a = <string>b; // 类型断言的另一种方式

11. Void 类型 ​

TypeScript 中的 void 表示没有任何类型,当一个函数没有返回值时候,通常会见到返回值类型为void,例如:

ts
function fn(): void {
  console.log("这是一段信息");
}

声明一个 void 类型的变量没有什么大用,因为在严格模式下,你只能为它赋予 undefined 和 null:

ts
let a: void = undefined;

12. Never 类型 ​

never 类型是其他类型(包括 null 和 undefine)的子类型,代表着从来不会出现的值,意味着声明 never 类型的变量只能被 never 类型所赋值; never 类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never。 例如:

ts
let c: never; //c不能被任何值赋值,包括null和undefiend,指不会出现的值
c = (() => {
  throw new Error("error!!");
})(); //可以这样赋值

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
  return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
  while (true) {}
}

13. null 和 undefined ​

TypeScript 里,undefined 和 null 两者有各自的类型分别为 undefined 和 null。 和 void 相似,它们的本身的类型用处不是很大:

ts
let u: undefined = undefined;
let n: null = null;

虽然为变量指定了类型,但是如果不赋值的话默认该变量还是 undefined 类型,如果没有指定 undefined 直接使用该变量的话会报错,只有自己指定的是 undefined 类型才不会报错

ts
let unde: undefined;
console.log(unde); // 报错

let unde2: undefined;
unde2 = undefined;
console.log(unde2); // 正确 输出undefined

let unde3: number;
console.log(unde3); // 报错

let unde4: undefined | number;
console.log(unde4); // 正确 输出undefined

14. object 类型 ​

object 表示非原始类型,也就是除 number,string,boolean,symbol(TypeScript 中的 Symbol 同 JS 完全一样,在这里是没有讲的),null 或 undefined 之外的类型 使用 object 类型,就可以更好的表示像 Object.create 这样的 API

ts
let stu: object = { name: "张三", age: 18 };
console.log(stu); //{name: "张三", age: 18}
//也可以
let stu: {} = { name: "张三", age: 18 };
console.log(stu);
ts
declare function create(o: object | null): void;
//declare是一个声明的关键字,可以写在声明一个变量时解决命名冲突的问题

create({ prop: 0 }); // OK
create(null); // OK

create(42); // 报错
create("string"); // 报错
create(false); // 报错
create(undefined); // 报错

14.1 对象解构 ​

ts
let person = {
  name: "ldh",
  gender: "zxy",
};

let { name, gender } = person;

14.2 对象展开运算符 ​

ts
let person = {
  name: "Semlinker",
  gender: "Male",
  address: "Xiamen",
};

// 组装对象
let personWithAge = { ...person, age: 33 };

// 获取除了某些项外的其它项
let { name, ...rest } = person;

15. 函数的类型 ​

15.1 函数声明 ​

ts
function sum(x: number, y: number): number {
  return x + y;
}

15.2 函数表达式 ​

采用函数表达式接口定义函数的方式时,对等号左侧进行类型限制,可以保证以后对函数名赋值时保证参数个数、参数类型、返回值类型不变。

ts
let mySum: (x: number, y: number) => number = function (x, y) {
  return x + y;
};

或者

ts
let fn2 = function (a: number, b: number): number {
  return a + b;
};

或者

ts
let fn3 = (a: number, b: number): number => a + b;

或者

ts
let mySum: (x: number, y: number) => number = function (
  x: number,
  y: number
): number {
  return x + y;
};

// `(x: number, y: number) => number` 不是箭头函数,别看错了,而是ts函数定义

注意:

  • 在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。不要与与 ES6 的箭头函数混淆了。

15.3 用接口定义函数类型 ​

ts
interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc = function (source: string, subString: string) {
  // OK
  let result = source.search(subString);
  return result > -1;
};

15.4 函数中的 this 声明 ​

TypeScript 会通过代码流分析来推断出 this 在函数中应该是什么,我们也可以明确指定函数中的 this 应是何种类型。

ts
interface Obj {
  fn: (this: Obj, name: string) => void;
}

let obj: Obj = {
  fn(name: string) {},
};

obj.fn("小白"); // OK

因为 JavaScript 规范规定你不能有一个名为 this 的参数,所以 TypeScript 使用这个语法空间来让你在函数体中声明 this 的类型。

注意:这个 this 类型声明必须放在参数的首位:

ts
interface Obj {
  fn: (name: string, this: Obj) => void; // Error:A 'this' parameter must be the first parameter
}

一个完整的例子:

ts
interface Obj {
  fn: (this: Obj, name: string) => void;
}

let obj: Obj = {
  fn(name: string) {},
};

let rab: Obj = {
  fn(name: string) {},
};

obj.fn("小白"); // OK
obj.fn.call(rab, "小白"); // OK
obj.fn.call(window, "小白"); // Error: this 应该为 Obj 类型,因为此处实在node环境下执行

15.5 函数的可选参数 ​

注意点:可选参数后面不允许再出现必需参数

ts
// lastName为可选参数
function buildName(firstName: string, lastName?: string) {
  if (lastName) {
    return firstName + " " + lastName;
  } else {
    return firstName;
  }
}
let tomcat = buildName("Tom", "Cat");
let tom = buildName("Tom");

15.6 参数默认值 ​

ts
// lastName参数的默认值是'Cat',同js,若不传则取Cat
function buildName(firstName: string, lastName: string = "Cat") {
  return firstName + " " + lastName;
}
let tomcat = buildName("Tom", "Cat");
let tom = buildName("Tom");

15.7 剩余参数 ​

ts
function push(array: any[], ...items: any[]) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a = [];
push(a, 1, 2, 3);

15.8 函数重载(声明合并) ​

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。 重载的概念在学 JAVA(JAVA 中的重载)的时候接触到的,JS 是没有这个概念的,TS 的重载个人感觉更应该称之为函数签名重载。因为最后函数实现的内部还是依赖判断类型来处理,前面的函数定义只是为了精确表达输入类型对应的输出类型。

例如:

ts
function fn(x: number): number;
function fn(x: string): string;
function fn(x: number | string): number | string | void {
  if (typeof x === "number") {
    return Number(x.toString().split("").reverse().join(""));
  } else if (typeof x === "string") {
    return x.split("").reverse().join("");
  }
}

在以上代码中,我们为 fn 函数提供了多个函数类型定义,从而实现函数的重载。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。

ts
class Calc {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: string, b: number): string;
  add(a: number, b: string): string;
  add(a: Combinable, b: Combinable) {
    if (typeof a === "string" || typeof b === "string") {
      return a.toString() + b.toString();
    }
    return a + b;
  }
}

const calc = new Calc();
const result = calc.add("Semlinker", " Kakuqo");

这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calc 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。

二、高级类型 ​

1. 联合类型| 交叉类型& ​

联合类型与交叉类型很有关联,但是使用上却完全不同, 我们使用的用竖线隔开每一个类型参数的时候使用的就是联合类型

  • 联合类型: 表示一个值可以是几种类型之一。 用竖线 | 分隔每个类型,所以 number | string | boolean 表示一个值可以是 number,string,或 boolean,如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员
  • 交叉类型: 交叉类型是将多个类型合并为一个类型,所以它包含了所需的所有类型的特性。用 & 连接。

简单使用,例如:

联合类型

ts
let flag: string | number | boolean; // 允许flag类型是string 或 number 或 boolean,不能是别的
flag = "成功";
flag = 1;
flag = true;

let flag2: 100 | "100"; // 允许flag2的值是100 或 '100'
flag2 = 100;
flag2 = "100";

// 函数参数
function fn(a: string | number): void {
  console.log(a);
}

// 也可以用类型别名来简化操作 (类型别名下面会说到)
type myType = string | number | boolean;
let flag3: myType;
flag3 = 123;
flag3 = "你好";
flag3 = false;
function fn2(b: myType) {}

交叉类型 例如:变量 a 既是 A 类型,同时也是 B 类型

ts
interface A {
  name: string;
  age: number;
}
interface B {
  name: string;
  gender: string;
}
let a: A & B = {
  // OK
  name: "小白",
  age: 18,
  gender: "男",
};

2. 类型推论 ​

如果没有明确的指定类型,那么 TypeScript 会依照类型推论的规则推断出一个类型。

例如:

ts
let num = 123;
num = true; // 错误

如上面的例子, 由于声明变量 num 时候,我们没有给变量显示的指定一个类型,此时 ts 会根据声明时候的赋值推断出 num 类型为 number,然后隐式的给 num 定为 number 类型,所以后面的赋值 true 是错误的操作

若声明变量时,不指定类型,也不赋值, 不管之后有没有赋值,ts 会将类型推断为 any,例如:

ts
let num; // num的ts类型为any
num = 123; // ok
num = "234"; // ok
num = true; // ok
console.log(num); // true

3. 类型断言 ​

类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构, 它没有运行时的影响,只是在编译阶段起作用,TypeScript 会假设程序员已经检查过

类型断言作用:可以再编译时候,告诉编译器变量的实际类型。

两种实现方式:

ts
// 尖括号 语法
let valStr: any = "周杰伦";
let strleng: number = (<string>valStr).length;

// as 语法
let someValue: any = "刘德华";
let strLength: number = (someValue as string).length;

注意: 两种形式是等价的,但是当在 TypeScript 里使用 JSX 时,只有 as 语法断言是被允许的

类型断言的用途:

  • (1)将一个联合类型断言为其中一个类型
  • (2)将一个父类断言为更加具体的子类
  • (3)将任何一个类型断言为 any
  • (4)将 any 断言为一个具体的类型

4. 接口 ​

什么是接口? 接口: 接口就是一个规范,是对某个类的限制,只要满足这个规范,就能使用,只要实现了某个接口(由接口申明某个对象或 class),就说明满足了这个规范,对象或 class 就能在指定定场景中使用。 TS 里的接口与后端接口有些类似,也可以理解为定义一些参数,规定变量里面有什么参数,参数是什么类型,使用时就必须有这些对应类型的参数,多参数或者少参数、参数类型不对都会报错。更简单的,你可以理解为这就是在定义一个较为详细的对象类型。

接口用来定义一个类的结构,定义一个类中应该包含那些二属性和方法 接口也可以当作类型声明去使用 接口可以重复声明,最后 ts 会合并相同的接口 接口只定义对象的结构,属性和方法都不能有实际的值或方法体, 接口和抽象类很相似,所有的方法都是抽象方法

定义类时,可以使用接口实现一个类 类继承用关键字 extends,类实现接口用关键字 implements

4.1 对象的模板 ​

ts
interface Person {
  name: string;
  age: number;
}
// person 对象的字段和类型必须和上面上面申明的一模一样,否则都会报错
let person: Person = {
  name: "小白",
  age: 16,
};

4.2 可选 ? 和 只读属性 readonly ​

ts
interface Person {
  name: string;
  age?: number; // 可选属性
  readonly hobby: string; // 只读属性
}
let person: Person = {
  name: "小白",
  age: 16,
  hobby: "玩",
};

person.name = "小黄人"; // ok
person.hobby = "吃"; // 报错 无法分配到 "hobby" ,因为它是只读属性
console.log(person);

只读属性用于限制只能在对象刚刚创建的时候修改其值 此外 TypeScript 还提供了 ReadonlyArray<T> 类型,它与 Array<T> 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。

ts
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

4.3 任意属性[] ​

有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。

ts
interface Person {
    name:string,
    age?:number, // 可选属性
    readonly hobby: string, // 只读属性
    [propName:string]:any  // 任意属性
}

// 由于定义了任意属性,person 多了三个上面定义时候没有的属性,也是对的
let person:Person = {
    name:'小白',
    hobby: "玩",
    auth:"http://www,baidu.com",
    status:true
}

4.4 接口声明合并 ​

接口中的属性在合并时会简单的合并到一个接口中:

ts
interface Alarm {
  price: number;
}
interface Alarm {
  weight: number;
}

相当于

ts
interface Alarm {
  price: number;
  weight: number;
}

注意:

接口中方法的合并,与函数的合并一样 合并时,若有相同的属性,类型必须是一致的,否则编译报错

ts
interface Alarm {
  price: number;
  count: number;
  alert(s: string): string;
}
interface Alarm {
  price: number; // 虽然重复了,但是类型都是 `number`,所以不会报错
  weight: number;
  count: string; // 类型不一致,会报错
  alert(s: string, n: number): string; // 同函数重载,两个而都会保存,不报错
}

5. 字面量类型 ​

字面量类型允许指定类型必须的固定值。在实际应用中,字面量类型可以与联合类型,类型保护和类型别名很好的配合。通过结合使用这些特性,可以实现类似枚举类型

ts
type Easing = "ease-in" | "ease-out" | "ease-in-out";

let animate: Easing;
animate = "fade"; // Error
animate = "ease-in"; // true
animate = "ease-out"; // true
animate = "ease-in-out"; // true

字面量类型还可以用于区分函数重载

ts
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
  // ... code goes here ...
}

6. 类型别名 ​

类型别名就是给一种类型起个别的名字,之后只要使用这个类型的地方,都可以用这个名字作为类型代替。它只是起了一个名字,并不是创建了一个新类型。使用 type 关键字来定义

ts
type Message = string | string[];

let greet = (message: Message) => {
  // ...
};

7. 接口与类型别名的区别 ​

7.1 Objects/Functions ​

接口:

  • 对象接口
ts
interface Point {
  x: number;
  y: number;
}
  • 函数接口
ts
interface  {
  (x: number, y: number): void;
}

类型别名

ts
// 对象别名
type Point = {
  x: number;
  y: number;
};
// 函数别名
type SetPoint = (x: number, y: number) => void;

7.2 其他类型 ​

与接口类型不一样,类型别名可以用于一些其他类型,比如原始类型、联合类型和元组:

ts
// p原始类型
type Name = string;

// 对象
type PartialPointX = { x: number };
type PartialPointY = { y: number };

// 联合类型
type PartialPoint = PartialPointX | PartialPointY;

// tuple来类型
type Data = [number, string];

7.3 Extend(类型继承) ​

a. 接口和类型别名都能够被扩展,但语法有所不同 b. 接口和类型别名不是互斥的 c. 接口可以扩展类型别名,而反过来是不行的。

(1)Interface extends interface

ts
interface PartialPointX {
  x: number;
}
interface Point extends PartialPointX {
  y: number;
}

(2)类型别名 extends 类型别名

ts
type PartialPointX = { x: number };
type Point = PartialPointX & { y: number };

(3)Interface extends 类型别名

ts
type PartialPointX = { x: number };
interface Point extends PartialPointX {
  y: number;
}

(4)类型别名 extends interface

ts
interface PartialPointX {
  x: number;
}
type Point = PartialPointX & { y: number };

7.4 Implements ​

类可以以相同的方式实现接口或类型别名,但类不能实现使用类型别名定义的联合类型

ts
interface Point {
  x: number;
  y: number;
}

class SomePoint implements Point {
  x = 1;
  y = 2;
}

type Point2 = {
  x: number;
  y: number;
};

class SomePoint2 implements Point2 {
  x = 1;
  y = 2;
}

type PartialPoint = { x: number } | { y: number };

// A class can only implement an object type or
// intersection of object types with statically known members.
// Error
class SomePartialPoint implements PartialPoint {
  x = 1;
  y = 2;
}

7.5 类型合并 ​

与类型别名不同,接口可以定义多次,会被自动合并为单个接口

ts
interface Point {
  x: number;
}
interface Point {
  y: number;
}

const point: Point = { x: 1, y: 2 };

8. Class ​

8.1 对比 JS 类与 TS 类 ​

同 js 里面一样,ts 里面也是使用 class 关键字来声明类,只是,ts 里面的类同 js 里面的类有一些不同之处,具体如下:

首先,在 ES2016(ES5)之前,不存在类的概念,只存在构造函数,要实现一个类,需要自己用 Function prototype 的方式来实现,这种实现方式与 c# java 等面向对象语言中实际意义的类差别很大,不好理解。 ​

到了 ES6,加入了 class,更好的贴近于面向对象,但是我们知道,class 在大部分浏览器里面是无法直接使用的,于是就需要借助 babel 等工具,将 class 转化为 ES5 中的构造函数,以便适配浏览器。

typeScript 中也有 class 的概念,并且可以在 ts 中使用很新的 ES 语法,这些语法不需要借助 babel,typescript 编译器 tsc 都会将其编译成 es5 甚至 es3。

1、类 class 的类型 本质上是一个函数; 类本身就指向自己的构造函数。 2、一个类必须有 constructor 方法,如果没有显示定义,一个空的 constructor 方法会被默认添加 3、我们在 ES6 的时候,实例属性都是定义在 constructor()方法里面, 在 ES7 里 我们可以直接将这个属性定义在类的最顶层,其它都不变,去掉 this; 4、通过代码我们也可以发现,new 类的时候就相当于 new 构造函数 5、调用类上面的方法就是调用原型上的方法 6、在类的实例上面调用方法,其实就是调用原型上的方法

8.2 继承 ​

1、使用继承来扩展现有的类,是面向对象的三大特性之一(封装,继承,多态) 2、基类,父类,超类是指被继承的类,派生类,子类是指继承于基类的类 3、ts 中类继承类似于传统面向对象编程语言中的继承体系 ,使用 extends 关键字继承,类中 this 表示此当前对象本身,super 表父类对象。子类构造函数中第一行代码调用父类构造函数完成初始化,然后再进行子类的进一步初始化。子类中可以访问父类(public、protected)的成员属性、方法 4、派生类包含了 constructor; ts 规定只要派生类里面自定义了一个 constructor 函数就必须在使用 this 前,调用一下 super 方法 (1)ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this));ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this (2)因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象

8.3 js 中 class 与 ts 中 class 对比 ​

ES5 中的模拟 class 示例:

js
function Person(name) {
  this.name = name;
}
Person.prototype.sayName = function () {
  console.log("I am" + this.name);
};

var kb = new Person("kebi");
lb.sayName(); // I am kebi

ES6 中的 class 示例:

js
class Person {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log("I am" + this.name);
  }
}

let kb = new Person("kebi");
kb.sayName(); // I am kebi

TS 中的 class 示例:

ts
class Greeter {
  // 静态属性
  static cname: string = "Greeter";
  // 成员属性
  greeting: string;

  // 构造函数 - 执行初始化操作
  constructor(message: string) {
    this.greeting = message;
  }

  // 静态方法
  static getClassName() {
    return "Class name is Greeter";
  }

  // 成员方法
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

以上对比可以看出,js 中的 class 与 ts 中的 class 十分相似,只不过 ts 增加了属性类型定义。

8.4 TS 中 class 的一些修饰符 ​

ts 类中修饰符分为 3 种:

  • public : 公有(所有)默认;
  • protected:保护 (父类+子类);
  • private: 私有(本类)

8.4.1 public 修饰符 ​

属性修饰符默认为 public 公共的,即类的属性、方法可以在外部访问,也可以显示声明

ts
class Animal {
  public name: string;
  public constructor(theName: string) {
    this.name = theName;
  }
  public move(distanceInMeters: number) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

8.4.2 private 修饰符 ​

private 与 public 相反,表示私有修饰符,即类的属性、方法不可以在外部访问;

若构造函数(constructor)被 private 修饰, 该类不允许被继承或者实例化;只允许被继承

ts
// static 是将属性添加到class本身上,不是原型上,所以不能通过实例来访问
class Animal {
  public static age: number = 18;
  private static title: string = "小白";
}

Animal.age; // OK
Animal.title; // Error

8.4.3 protected 修饰符 ​

protected 修饰符与 private 修饰符的行为很相似,但有一点不同,protected 成员在派生类中仍然可以访问。注意,这里是派生类中(子类中),而不是父类实例、子类实例。

ts
class Person {
  private name: string = "姓名"; // 私有属性,不可派生
  private static age: number = 100; // 静态属性 不可派生
  protected sex: 0 | 1 | 2 = 2; // 共有属性 可派生
  protected static height: number = 170; // 静态属性,可派生
}

class Man extends Person {
  getPorp() {
    console.log(this.name); // Error
    console.log(Man.age); // Error

    console.log(this.sex); // ok
    console.log(Man.height); // ok
  }
}

8.4.4 参数属性修饰符 ​

我们也可以在类的内部方法上对参数使用 public、private、protected 修饰符,它的作用是可以更方便地让我们在一个地方定义并初始化一个成员

ts
class Animal {
  constructor(
    public name: string,
    private age: number,
    protected sex: string
  ) {}
}

等同于

ts
class Animal {
  public name: string;
  private age: number;
  protected sex: string;
  constructor(name: string, age: number, sex: string) {
    this.name = name;
    this.age = age;
    this.sex = sex;
  }
}

8.4.5 abstract 抽象类 ​

抽象类做为其它派生类的基类使用, 不允许被实例化。 不同于接口,抽象类可以包含成员的实现细节。 抽象类不能被实例化,因为它里面包含一个或多个抽象方法。

所谓的抽象方法,是指不包含具体实现的方法: 方法前面加上 abstract 关键字就是抽象方法 抽象方法必须定义到抽象类中 抽象方法不包含方法体 抽象方法必须再子类中重写,否则会报错

abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

例如:

ts
abstract class Animal {
  abstract makeSound(): void;
  move(): void {
    console.log("roaming the earth...");
  }
}

let animal = new Animal(); // Error: 抽象类不允许被实例化

抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract 关键字并且可以包含访问修饰符。

例如:

ts
abstract class Department {
    constructor(public name: string) {
    }
    printName(): void {
        console.log('Department name: ' + this.name);
    }
    abstract printMeeting(): void; 、、 抽象返回不包含方法体
}
class AccountingDepartment extends Department {
    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }
    printMeeting(): void {  // 抽象方法,必须在派生类中实现
        console.log('The Accounting Department meets each Monday at 10am.');
    }
    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}
let department: Department; // OK:允许创建一个对抽象类型的引用
department = new Department(); // Error: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // OK:允许对一个抽象子类进行实例化和赋值
department.printName(); // OK
department.printMeeting(); // OK
department.generateReports(); // Error: 方法在声明的抽象类中不存在

8.4.6 类实现接口 ​

TypeScript 也能够用接口来明确的强制一个类去符合某种契约。

意思也就是,我们也可以用类去实现接口,这里使用关键字 implements:

ts
interface Title {
  title: string;
}
class title implements Title {
  title: string = "兔兔";
  age: number = 18; // 在实现接口的基础上,也可以添加其他的属性和方法
}

一个类可以实现多个接口:

ts
interface Age {
  age: number;
}

interface Title {
  title: string;
}
class title implements Title, Age {
  title: string = "兔兔";
  age: number = 18;
}

注意事项:

  • 当类实现一个接口时,只对实例部分进行类型检查

对比:抽象类与接口的区别 ​

类可以继承抽象类,类也可以实现接口,那么抽象类与接口的异同如下:

  • (1)抽象类是用来捕捉子类的通用特性的,而接口则是抽象方法的集合
  • (2)抽象类不能被实例化,只能被用作子类的超类,是被用来创建继承层级里子类的模板,而接口只是一种形式,接口自身不能做任何事情。
  • (3)抽象类可以有默认的方法实现,子类使用 extends 关键字来继承抽象类,如果子类不是抽象类的话,它需要提供抽象类中所有声明方法的实现。而接口完全是抽象的,它根本不存在方法的实现,子类使用关键字 implements 来实现接口,它需要提供接口中所有声明方法的实现。

8.4.7 接口继承类 ​

我们知道接口只可以继承接口,因为它们是同一类别的,对于接口继承类,官方的解释是:在 TS 中声明一个类的时候,同时也声明了一个类的实例的类型。

例如:

ts
class Point {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

interface Point3d extends Point {
  z: number;
}

let point3d: Point3d = { x: 1, y: 2, z: 3 };

所以,我们可以声明一个变量为 Greeter 类型:

ts
let greeter: Greeter = new Greeter("world");

这里冒号后面的 Greeter 此时就是作为类的实例类型而存在的,new 后面的 Greeter 作为构造函数存在。

更进一步,我们知道 class 本质是 function 的语法糖:

ts
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

转译为 ES5:

js
"use strict";
var Greeter = /** @class */ (function () {
  function Greeter(message) {
    this.greeting = message;
  }
  Greeter.prototype.greet = function () {
    return "Hello, " + this.greeting;
  };
  return Greeter;
})();

这个类的实例类型 Greeter 就对应转译的 ES5 中构造函数 Greeter 的实例类型,既然指实例类型,所以在接口继承类的时候,构造函数、静态属性、静态方法是不被包含的(实例的类型当然不应该包括构造函数、静态属性或静态方法):

ts
class Point {
  /** 静态属性,坐标系原点 */
  static origin = new Point(0, 0);
  /** 静态方法,计算与原点距离 */
  static distanceToOrigin(p: Point) {
    return Math.sqrt(p.x * p.x + p.y * p.y);
  }
  /** 实例属性,x 轴的值 */
  x: number;
  /** 实例属性,y 轴的值 */
  y: number;
  /** 构造函数 */
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
  /** 实例方法,打印此点 */
  printPoint() {
    console.log(this.x, this.y);
  }
}

同时声明的类型等同于:

ts
interface PointInstanceType {
  x: number;
  y: number;
  printPoint(): void;
}

let p1: Point;
let p2: PointInstanceType; // p1 的类型与 p2 等价

8.4.8 访问器get和set ​

在 TypeScript 中,我们可以通过 getter 和 setter 方法来实现数据的封装和有效性校验,防止出现异常数据。

ts
let passcode = "Hello TypeScript";

class Employee {
  private _fullName: string;

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (passcode && passcode == "Hello TypeScript") {
      this._fullName = newName;
    } else {
      console.log("Error: Unauthorized update of employee!");
    }
  }
}

let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
  console.log(employee.fullName);
}

8.4.9 类方法重载 ​

函数有重载,对于类的方法来说,它也支持重载。比如,在以下示例中我们重载了 ProductService 类的 getProducts 成员方法

ts
class ProductService {
  getProducts(): void;
  getProducts(id: number): void;
  getProducts(id?: number) {
    if (typeof id === "number") {
      console.log(`获取id为 ${id} 的产品信息`);
    } else {
      console.log(`获取所有的产品信息`);
    }
  }
}

const productService = new ProductService();
productService.getProducts(666); // 获取id为 666 的产品信息
productService.getProducts(); // 获取所有的产品信息

9. 泛型 ​

9.1 介绍 ​

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。 简单说就是:泛指的类型,不确定的类型,可以理解为一个占位符(使用 T 只是习惯,使用任何字母都行)

以下是常见泛型变量代表的意思

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

例如:

js
function fn(arg) {
  return arg;
}

定义一个函数 fn,但是我们不确定这个函数参数的类型,返回值类型,

有的人会说用 any, any 是可以解决问题,但是用了 any 后,就等于关闭了 TS 类型检查

有一种笨办法,函数重载(声明合并),如下:

ts
// Bad
function fn(arg: boolean): boolean;
function fn(arg: number): number;
function fn(arg: string): string;
function fn(arg) {
  return arg;
}

以上代码虽然不报错,但存在的问题是, JS 提供多少种类型,就需要复制多少份代码,这种复制粘贴增加了出错的概率,使得代码难以维护,牵一发而动全身

TS 里面提供了泛型,就是来解决这一问题的。 以上的函数用 泛型来重构 如下:

ts
// Good
function fn<T>(arg: T): T {
  return arg;
}

fn<string>("hello"); // 正确
fn(123); // 正确
fn([123]); // 正确

很简单:T 是一个抽象类型,只有在调用的时候才确定它的值,在 fn 后面用 <T> 尖括号声明泛型的类型,该类型只有在函数 fn 调用时候才能被确定,函数的形参,返回值都可以用这个类型。

编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T,而不需要开发人员显式指定它们,避免了一堆重复代码的产生

借用网络上的 2 张图可以清晰的理解泛型

9.2 泛型约束 extends ​

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

ts
function loggingIdentity<T>(arg: T): T {
  console.log(arg.length);
  return arg;
}

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

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

ts
interface Length {
  length: number;
}

function fn<T extends Length>(arg: T): T {
  console.log(arg.length);
  return arg;
}
fn("123"); // 正确 因为string本身就有length;
fn({ length: 5 }); // 正确
// or
fn<Length>("hello"); // 正确

fn(456); // 编译报错: 传入的 arg 不包含 length,那么在编译阶段就会报错

我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。

多个类型参数之间也可以互相约束:

ts
function copyFields<T extends U, U>(target: T, source: U): T {
  for (let id in source) {
    target[id] = (<T>source)[id];
  }
  return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 });

上例中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。

9.3 多个类型参数 ​

定义泛型的时候,可以一次定义多个类型参数:

ts
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

swap([7, "seven"]); // ['seven', 7]

上例中,我们定义了一个 swap 函数,用来交换输入的元组。

9.4 泛型接口 ​

ts
interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<boolean>; // 这里必须指定实际类型,(这里为boolean)
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    console.log(result);

    return result;
}

createArray(3, true); // [ true, true, true]

9.5 泛型类 ​

ts
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;
};

注意,此时在使用泛型接口的时候,需要定义泛型的类型

9.6 泛型参数的默认类型 ​

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

ts
function createArray<T = string>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

9.7 泛型工具类型 ​

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者可以更好的学习其它的工具类型。

typeof ​

typeof 的主要用途是在类型上下文中获取变量或者属性的类型

获取普通对象的类型

ts
interface Person {
  name: string;
  age: number;
}
const sem: Person = { name: "semlinker", age: 30 };
type Sem = typeof sem; // 相当于:type Sem = Person

获取嵌套对象的类型

ts
const Message = {
  name: "jimmy",
  age: 18,
  address: {
    province: "四川",
    city: "成都",
  },
};
type message = typeof Message;
// 相当于
/*
 type message = {
    name: string;
    age: number;
    address: {
        province: string;
        city: string;
    };
}
*/

获取函数类型

ts
function toArray(x: number): Array<number> {
  return [x];
}
type Func = typeof toArray; // 相当于 (x: number) => number[]

keyof ​

keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型

ts
interface Person {
  name: string;
  age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number

用 keyof 可以更好的定义数据类型

ts
function get<T extends object, K extends keyof T>(o: T, name: K): T[K] {
  return o[name];
}

在 TypeScript 中支持两种索引签名,数字索引和字符串索引:

ts
interface StringArray {
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string;
}

interface StringArray1 {
  // 数字索引 -> keyof StringArray1 => number
  [index: number]: string;
}

为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number。

keyof 也支持基本数据类型:

ts
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"

in ​

in 用来遍历枚举类型:

ts
type Keys = "a" | "b" | "c";

type Obj = {
  [p in Keys]: any;
};
// -> { a: any, b: any, c: any }

infer ​

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

ts
type Returntype<T> = T extends (...args: any[]) => infer R ? R : any;

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

extends ​

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束

ts
interface Lengthwise {
  length: number;
}

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

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

ts
loggingIdentity(3); // Error, number doesn't have a .length property 3没有length属性,用'3'可以

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

ts
loggingIdentity({ length: 10, value: 3 });
// or
loggingIdentity("3");

映射类型 ​

根据旧的类型创建出新的类型, 我们称之为映射类型 自我理解:映射是 Partial、Required 等内置工具类型的核心原理

比如我们定义一个接口

ts
// 原接口,每个属性都必有,且不是只读属性
interface TestInterface {
  name: string;
  age: number;
}

开始映射 ↓,如下:

(1)我们把上面定义的接口里面的属性全部变成可选

ts
// 添加了? ,映射为可选参数
type OptionalTestInterface<T> = {
  [p in keyof T]?: T[p];
};

type newTestInterface = OptionalTestInterface<TestInterface>;
// type newTestInterface = {
//    name?:string,
//    age?:number
// }

(2)比如我们再加上只读

ts
// 添加了readonly,映射为只读属性
type OptionalTestInterface<T> = {
  readonly [p in keyof T]?: T[p];
};
// 最终得到如下
type newTestInterface = OptionalTestInterface<TestInterface>;
// type newTestInterface = {
//   readonly name?:string,
//   readonly age?:number
// }

由于生成只读属性和可选属性比较常用, 所以 TS 内部已经给我们提供了现成的实现 Readonly / Partial,如下介绍.

Partial<T\> ​

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

例如:

ts
type Todo = {
  title: string;
  description: string;
};

// 正确
let todo: Partial<Todo> = {
  title: "234",
};

那么

ts
Partial<Todo>;

就相当于

ts
type Todo = {
  title?: string;
  description?: string;
};

原理

ts
/**
 * 源码是这样定义的
 * node_modules/typescript/lib/lib.es5.d.ts
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

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

示例

ts
interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
  title: "Learn TS",
  description: "Learn TypeScript",
};

const todo2 = updateTodo(todo1, {
  description: "Learn TypeScript Enum",
});

使用

ts
Partial<T>; // T 就是实际的能被 `keyof T`取到所有属性名是对象

Required<T\> ​

Required<T>将类型的属性变成必选,同Partial相反

原理

ts
type Required<T> = {
  [P in keyof T]: T[P];
};

Readonly<T> ​

Readonly<T> 的作用是将某个类型所有属性变为只读属性,也就意味着这些属性不能被重新赋值。

原理

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

例如

ts
interface Todo {
  title: string;
}

const todo: Readonly<Todo> = {
  title: "Delete inactive users",
};

todo.title = "Hello"; // Error: cannot reassign a readonly property

Pick<T,prop\> ​

Pick<T,prop> 从某个类型 T 中挑出一些属性 prop 出来

原理

Ts
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

例如:

ts
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

可以看到 NewUserInfo 中就只有个 name 的属性了。

Record<K extends keyof any, T\> ​

Record<K extends keyof any, T> 的作用是将 K 中所有的属性的值转化为 T 类型。

原理

ts
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

示例:

ts
interface PageInfo {
  title: string;
}

type Page = "home" | "about" | "contact";

const x: Record<Page, PageInfo> = {
  about: { title: "about" },
  contact: { title: "contact" },
  home: { title: "home" },
};

ReturnType<T\> ​

用来得到一个函数 T的返回值类型

原理:

ts
type ReturnType<T extends (...args: any[]) => any> = T extends (
  ...args: any[]
) => infer R
  ? R
  : any;

infer 在这里用于提取函数类型的返回值类型。ReturnType<T> 只是将 infer R 从参数位置移动到返回值位置,因此此时 R 即是表示待推断的返回值类型。

例如:

ts
type Func = (value: number) => string;
const foo: ReturnType<Func> = "1";

ReturnType 获取到 Func 的返回值类型为 string,所以,foo 也就只能被赋值为字符串了

Exclude<T, U\> ​

Exclude<T, U> 的作用是将某个类型中属于另一个的类型移除掉。

原理:

ts
type Exclude<T, U> = T extends U ? never : T;

如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是将 T 中某些属于 U 的类型移除掉。

例如:

ts
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

Extract<T, U\> ​

Extract<T, U> 的作用是从 T 中提取出 U。

原理

ts
type Extract<T, U> = T extends U ? T : never;

例如:

ts
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () =>void

Omit<T, K extends keyof any\> ​

Omit<T, K extends keyof any> 的作用是使用 T 类型中除了 K 类型的所有属性,来构造一个新的类型。

原理

ts
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

例如:

ts
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

NonNullable<T\> ​

NonNullable<T> 的作用是用来过滤类型中的 null 及 undefined 类型。

原理

ts
type NonNullable<T> = T extendsnull | undefined ? never : T;

例如:

ts
type NonNullable<T> = T extendsnull | undefined ? never : T;

Parameters<T\> ​

Parameters<T> 的作用是用于获得函数的参数类型组成的元组类型

原理

ts
type Parameters<T extends (...args: any) => any> = T extends ( ...args: infer P) => any ? P : never;

例如:

ts
type A = Parameters<() => void>; // []
type B = Parameters<typeofArray.isArray>; // [any]
type C = Parameters<typeofparseInt>; // [string, (number | undefined)?]
type D = Parameters<typeofMath.max>; // number[]

三、参考文档 ​

TypeScript Handbook(中文版)推荐

阿宝哥《一份不可多得的 TS 学习指南(1.8W 字)》

伟大的兔神《重学 TypeScript》

TypeScript 入门教程

TS 官方文档

2021 typescript 史上最强学习入门文章(2w 字)