Skip to content

TypeScript(以下简称 TS)是基于 JavsSript 语言的一种超集,本质上是向 JS 中加入了可选静态类型和基于类的面向对象编程。

TS 和 ECMAScript 之间的关系图如下图:

TS 和 ECMAScript 的关系

TS 和 JS 的区别:

TSJS
JavaScript 的超集用于解决大型项目的代码复杂性一种脚本语言,用于创建动态网页
在编译期间发现并纠正错误作为一种解释性语言,只能在运行时发现错误
强类型,支持动态和静态类型弱类型,无静态类型
最终编译成 JS,可被浏览器识别可直接运行在浏览器
支持模块、泛型、接口不支持
生态不是很大社区庞大

安装及使用 ​

安装 ​

shell
npm install -g typescript
# yarn
yarn global add typescript

编译 ​

创建 test.ts:

ts
const a: String = 'aaa';

执行编译,目录会增加编译的 JS 文件:

shell
tsc test
js
var a = 'aaa';

在线调试:TS Playground

工作流程:

TS 工作流程

通常 TS 代码在编译后都会进行一个打包处理,然后进行部署。

基础类型 ​

Boolean ​

ts
let isDone: boolean = false;
// ES5:var isDone = false;

Number ​

ts
let count: number = 10;
// ES5:var count = 10;

String ​

ts
let name: string = 'semliker';
// ES5:var name = 'semlinker';

Symbol ​

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

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

Array ​

ts
// 1. 直接定义
let list: number[] = [1, 2, 3];
// 2. 定义泛型
let list: Array<number> = [1, 2, 3];
// 3. 定义接口
interface NumberArray {
    [index: number]: number;
}
let list: NumberArray = [1, 2, 3];

// 类数组
function sum() {
    let args: IArguments = arguments;
}

// 任意类型数组
let list: any[] = ['example', 25, { website: 'http://example.com' }];

// 定义对象数组
const arr: { age: string }[] = [{ age: 'dell' }];
// 规定某种对象格式
type User = {
    name: string;
    age: number;
};
const arr3: User[] = [{ name: '12', age: 12 }];
// 甚至可以定义一个类对象数组
class Teacher {
    name: string;
    age: number;
}
const arr4: Teacher[] = [new Teacher(), { name: '12', age: 12 }];
// 二位数组 + 元祖定义
const demo1: [string, number, string][] = [
    ['1', 2, '1'],
    ['2', 3, '3']
];

// 解构数组
let arr: number[] = [0, 1, 2, 3];
let x: number;
let y: number;
let z: number;
[x, y, z] = arr;

Enum ​

1. 数字枚举 ​

ts
enum Direction {
    NORTH,
    SOUTH,
    EAST,
    WEST
}

let dir: Direction = Direction.NORTH;
// 默认情况下,NORTH 的初始值为0,其他成员递增
js
// 默认情况下编译上述 TS 得到的ES5代码
'use strict';
var Direction;
(function(Direction) {
    Direction[(Direction['NORTH'] = 0)] = 'NORTH';
    Direction[(Direction['SOUTH'] = 1)] = 'SOUTH';
    Direction[(Direction['EAST'] = 2)] = 'EAST';
    Direction[(Direction['WEST'] = 3)] = 'WEST';
})(Direction || (Direction = {}));
var dir = Direction.NORTH;

2. 字符串枚举 ​

ts
enum Direction {
    NORTH = 'NORTH',
    SOUTH = 'SOUTH',
    EAST = 'EAST',
    WEST = 'WEST'
}
let dirName = Direction[0]; // NORTH
let dirVal = Direction['NORTH']; // 0 这里会出现反向映射

编译后:

js
'use strict';
var Direction;
(function(Direction) {
    Direction['NORTH'] = 'NORTH';
    Direction['SOUTH'] = 'SOUTH';
    Direction['EAST'] = 'EAST';
    Direction['WEST'] = 'WEST';
})(Direction || (Direction = {}));

3. 常量枚举 ​

添加 const 关键词即可:

ts
const enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT',
}

const value = 'UP'
if (value === Direction.Up) {
    // do something
}

编译出来的 JS 代码会简洁很多,提高了性能:

js
'use strict';
const value = 'UP';
if (value === 'UP' /* Direction.Up */) {
    // do something
}

4. 异构枚举 ​

字符串和数字混合组合:

ts
enum Enum {
    A,
    B,
    C = 'C',
    D = 'D',
    E = 8,
    F
};

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

Any ​

普通类型在赋值过程中改变类型是不允许的:

ts
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;

// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.

如果是 any 类型,则允许被赋值为任意类型:

ts
let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;

any 类型的属性和方法是允许访问的,也允许调用任何方法。声明一个变量为 any 之后,对它的任何操作,返回的内容的类型都是任意值:

ts
let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);

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 类型的方法属性并不被允许访问

ts
let value: unknown;

value.foo.bar;  // Error
value.trim();   // Error
value();        // Error
new value();    // Error
value[0][1];    // Error

Tuple ​

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同:

ts
let tuple: [number, string] = [18, 'jimco'];

可以对元组使用数组的方法,比如使用 push 时,不会有越界报错:

ts
let tuple: [number, string] = [18, 'jimco'];

tuple.push(100);    // 但是只能 push 定义的 number 或者 string 类型
tuple.push(true);   // Error

Void ​

void 类型与 any 类型相反,它表示没有任何类型。比如函数没有明确返回值,默认返回 Void 类型:

ts
function welcome(): void {
    console.log('hello')
}

Never ​

never 类型表示的是那些永不存在的值的类型。

有些情况下值会永不存在,比如,

  • 如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值,因为抛出异常会直接中断程序运行
  • 函数中执行无限循环的代码,使得程序永远无法运行到函数返回值那一步
ts
// 异常
function fn(msg: string): never {
    throw new Error(msg);
}

// 死循环 千万别这么写,会内存溢出
function fn(): never {
    while (true) {}
}

never 类型是任何类型的子类型,也可以赋值给任何类型。

没有类型是 never 的子类型,没有类型可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never。

ts
let test1: never;
test1 = 'jimco'; // 报错,Type 'string' is not assignable to type 'never'
ts
let test1: never;
let test2: any;

test1 = test2; // 报错,Type 'any' is not assignable to type 'never'

Null or Undefined ​

ts
let u:undefined = undefined;    // undefined 类型
let n:null = null;              // null 类型

默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。

ts
let age: number = null;
let realName: string = undefined;

但是如果指定了 --strictNullChecks 标记,null 和 undefined 只能赋值给 void 和它们自身,不然会报错:

ts
// strictNullChecks
let age: number = null; // Error

函数类型 ​

TS 定义函数类型需要定义输入参数类型和输出类型,输出类型也可以忽略,因为 TS 能够根据返回语句自动推断出返回值类型:

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

add(1, 2);

函数没有明确返回值,默认返回 void 类型:

ts
function welcome(): void {
    console.log('hello');
}

函数表达式 ​

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

可选参数 ​

参数后加个问号,代表这个参数是可选的:

ts
function add(x:number, y:number, z?:number):number {
    return x + y;
}

add(1,2,3);
add(1,2);

注意:可选参数要放在函数入参的最后面,不然会导致编译错误。

默认参数 ​

ts
function add(x:number, y:number = 100):number {
    return x + y;
}

add(100);  // 200

跟 JS 的写法一样,在入参里定义初始值。

和可选参数不同的是,默认参数可以不放在函数入参的最后面:

ts
function add(x:number = 100, y:number):number {
    return x + y;
}

add(100); // Error: Expected 2 arguments, but got 1.

看上面的代码,add 函数只传了一个参数,如果理所当然地觉得 x 有默认值,只传一个就传的是 y 的话,就会报错。编译器会判定你只传了 x,没传 y。

函数赋值 ​

在 JS 中变量可以任意修改赋值,但在 TS 中不行,会报错:

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

add = 123; // Error: Type 'number' is not assignable to type '(x: number | undefined, y: number) => number'.

可以用下面这种方式定义一个函数:

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

let add2: (x:number, y:number) => number = add;

这有点像 es6 中的箭头函数,但不是箭头函数,TS 遇到 : 就知道后面的代码是写类型用的。当然,不用定义 add2 类型直接赋值也可以,TS 会在变量赋值的过程中,自动推断类型。

函数重载 ​

函数重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。

不同参数类型 ​

比如我们实现一个 add 函数,如果传入参数都是数字,就返回数字相加,如果传入参数都是字符串,就返回字符串拼接:

ts
function add(x: number[]): number
function add(x: string[]): string
function add(x: any[]): any {
    if (typeof x[0] === 'string') {
        return x.join();
    }
    if (typeof x[0] === 'number') {
        return x.reduce((acc, cur) => acc + cur);
    }
}

在 TS 中,实现函数重载,需要多次声明这个函数,前几次是函数定义,列出所有的情况,最后一次是函数实现,需要比较宽泛的类型,比如上面的例子就用到了 any。

不同参数个数 ​

假设这个 add 函数接受更多的参数个数,比如还可以传入一个参数 y,如果传了 y,就把 y 也加上或拼接上,就可以这么写:

ts
function add(x: number[]): number
function add(x: string[]): string
function add(x: number[], y: number[]): number
function add(x: string[], y: string[]): string
function add(x: any[], y?: any[]): any {
    if (Array.isArray(y) && typeof y[0] === 'number') {
        return x.reduce((acc, cur) => acc + cur) + y.reduce((acc, cur) => acc + cur);
    }
    if (Array.isArray(y) && typeof y[0] === 'string') {
        return x.join() + ',' + y.join();
    }
    if (typeof x[0] === 'string') {
        return x.join();
    }
    if (typeof x[0] === 'number') {
        return x.reduce((acc, cur) => acc + cur);
    }
}

console.log(add([1,2,3]));          // 6
console.log(add(['jimco', '18']));    // 'jimco,18'
console.log(add([1,2,3], [1,2,3])); // 12
console.log(add(['jimco', '18'], ['man', 'handsome'])); // 'jimco,18,man,handsome'

其实写起来挺麻烦的,后面了解泛型之后写起来会简洁一些,不必太纠结函数重载,知道有这个概念即可,平时一般用泛型来解决类似问题。

Interface ​

基本概念 ​

interface(接口) 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。定义 interface 一般首字母大写,代码如下:

ts
interface Person {
    name: string
    age: number
}

const p1: Person = {
    name: 'jimco',
    age: 18
}

属性必须和类型定义的时候完全一致。少写了属性,报错:

ts
const p1: Person = {
    name: 'jimco'
}
// Error: Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.

多写了属性,报错:

ts
const p1: Person = {
    name: 'jimco',
    age: 18,
    job: 'AtHome'
}
// Error: Type '{ name: string; age: number; job: string; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'job' does not exist in type 'Person'.

注意:interface 不是 JS 中的关键字,所以 TS 编译成 JS 之后,这些 interface 是不会被转换过去的,都会被删除掉,interface 只是在 TS 中用来做静态检查。

可选属性 ​

跟函数的可选参数是类似的,在属性上加个 ?,这个属性就是可选的,比如下面的 age 属性:

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

const p1: Person = {
    name: 'jimco',
}

只读属性 ​

如果希望某个属性不被改变,可以这么写:

ts
interface Person {
    readonly id: number
    name: string
    age: number
}

改变这个只读属性时会报错:

ts
const p1: Person = {
    id: 1,
    name: 'jimco',
    age: 18
};

p1.id = 567;
// Cannot assign to 'id' because it is a read-only property.

描述函数类型 ​

interface 也可以用来描述函数类型,代码如下:

ts
interface ISum {
    (x:number,y:number): number
}

const add: ISum = (num1, num2) => {
    return num1 + num2;
}

自定义属性 ​

上文中,属性必须和类型定义的时候完全一致,如果一个对象上有多个不确定的属性,怎么办?可以这么写:

ts
interface RandomKey {
    [propName: string]: string
}

const obj: RandomKey = {
    a: 'hello',
    b: 'jimco',
    c: 'welcome',
};

如果把属性名定义为 number 类型,就是一个类数组了,看上去和数组一模一样:

ts
interface LikeArray {
    [propName: number]: string
}

const arr: LikeArray = ['hello', 'jimco'];

arr[0];  // 可以使用下标来访问值

当然,不是真的数组,数组上的方法它是没有的:

ts
arr.push(1);
// Property 'push' does not exist on type 'LikeArray'.

鸭子类型 ​

看到这里,你会发现,interface 的写法非常灵活,它不是教条主义。用 interface 可以创造一系列自定义的类型。事实上, interface 还有一个响亮的名称: Duck Typing(鸭子类型)。

当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。 -- James Whitcomb Riley

这句话完美地诠释了 interface 的含义,只要数据满足了 interface 定义的类型,TS 就可以编译通过。举个例子:

ts
interface FunctionWithProps {
    (x: number): number
    fnName: string
}

FunctionWithProps 接口描述了一个函数类型,还向这个函数类型添加了 FnName 属性,这看上去完全是四不像,但是这个定义是完全可以工作的:

ts
const fn: FunctionWithProps = (x) => {
    return x;
}

fn.fnName = 'hello world';

事实上, React 的 FunctionComponent(函数式组件) 就是这么写的:

ts
interface FunctionComponent<P = {}> {
    (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
    propTypes?: WeakValidationMap<P> | undefined;
    contextTypes?: ValidationMap<any> | undefined;
    defaultProps?: Partial<P> | undefined;
    displayName?: string | undefined;
}

类 ​

我们知道, JS 是靠原型和原型链来实现面向对象编程的,ES6 新增了语法糖 class。TS 通过 public、private、protected 三个修饰符来增强了 JS 中的类。

在 TS 中,写法和 JS 差不多,只是要定义一些类型而已,我们通过下面几个例子来复习一下类的封装、继承和多态。

基本写法 ​

定义一个 Person 类,有属性 name 和 方法 speak:

ts
class Person {
    name: string
    constructor(name: string) {
        this.name = name;
    }
    speak() {
        console.log(`${this.name} is speaking`);
    }
}

const p1 = new Person('jimco'); // 新建实例

p1.name; // 访问属性和方法
p1.speak();

继承 ​

使用 extends 关键字实现继承,定义一个 Student 类继承自 Person 类:

ts
class Student extends Person {
    study() {
        console.log(`${this.name} needs study`);
    }
}

const s1 = new Student('jimco');

s1.study();

继承之后,Student 类上的实例可以访问 Person 类上的属性和方法。

super 关键字 ​

注意,上例中 Student 类没有定义自己的属性,可以不写 super,但是如果 Student 类有自己的属性,就要用到 super 关键字来把父类的属性继承过来。比如,Student 类新增一个 grade(成绩) 属性,就要这么写:

ts
class Student extends Person {
    grade: number
    constructor(name: string,grade:number) {
        super(name);
        this.grade = grade;
    }
}

const s1 = new Student('jimco', 100);

不写 super 会报错,这是 ES6 class 规范的要求。

多态 ​

子类对父类的方法进行了重写,子类和父类调同一个方法时会不一样。

ts
class Student extends Person {
    speak() {
        return `Student ${super.speak()}`;
    }
}

public ​

public,公有的,一个类里默认所有的方法和属性都是 public。比如上文中定义的 Person 类,其实是这样的:

ts
class Person {
    public name: string
    public constructor(name: string) {
        this.name = name;
    }
    public speak() {
        console.log(`${this.name} is speaking`);
    }
}

public 可写可不写,不写默认也是 public。

private ​

private,私有的,只属于这个类自己,它的实例和继承它的子类都访问不到。将 Person 类的 name 属性改为 private:

ts
class Person {
    private name: string
    public constructor(name: string) {
        this.name = name;
    }
    public speak() {
        console.log(`${this.name} is speaking`);
    }
}

实例访问 name 属性,会报错。继承它的子类访问 name 属性,也会报错:

ts
const p1 = new Person('jimco');
console.log(p1.name); // Error: Property 'name' is private and only accessible within class 'Person'.

class Student extends Person {
    speak() {
        return `Student ${this.name}`; // Error: Property 'name' is private and only accessible within class 'Person'.
    }
}

protected ​

protected 受保护的,继承它的子类可以访问,实例不能访问。将 Person 类的 name 属性改为 protected:

ts
class Person {
    protected name: string
    public constructor(name: string) {
        this.name = name;
    }
    public speak() {
        console.log(`${this.name} is speaking`);
    }
}

实例访问 name 属性,会报错:

ts
const p1 = new Person('jimco');
console.log(p1.name); // Error: Property 'name' is private and only accessible within class 'Person'.

class Student extends Person {
    speak() {
        return `Student ${this.name}`;
    }
}

static ​

static 是静态属性,可以理解为是类上的一些常量,实例不能访问。比如一个 Circle 类,圆周率是 3.14,可以直接定义一个静态属性:

ts
class Circle {
    static pi = 3.14
    public radius: number
    public constructor(radius: number) {
        this.radius = radius;
    }
    public calcLength() {
        return Circle.pi * this.radius * 2; // 计算周长,直接访问 Circle.pi
    }
}

实例访问,会报错:

ts
const c1 = new Circle(10);

console.log(c1.pi); // Error: Property 'pi' does not exist on type 'Circle'. Did you mean to access the static member 'Circle.pi' instead?

抽象类 ​

TS 通过 public、private、protected 三个修饰符来增强了 JS 中的类,其实 TS 还对 JS 扩展了一个新概念 —— 抽象类。

所谓抽象类,是指只能被继承,但不能被实例化的类,就这么简单。抽象类有两个特点:

  • 抽象类不允许被实例化
  • 抽象类中的抽象方法必须被子类实现

抽象类用一个 abstract 关键字来定义,我们通过两个例子来感受一下抽象类的两个特点。

抽象类不允许被实例化:

ts
abstract class Animal {}

const a = new Animal(); // Error: Cannot create an instance of an abstract class.

定义一个抽象类 Animal,初始化一个 Animal 的实例,直接报错。

抽象类中的抽象方法必须被子类实现:

ts
abstract class Animal {
    constructor(name:string) {
        this.name = name
    }
    public name: string
    public abstract sayHi():void
}

class Dog extends Animal {
    constructor(name:string) {
        super(name);
    }
}
// Error: Non-abstract class 'Dog' does not implement inherited abstract member 'sayHi' from class 'Animal'.

定义一个 Dog 类,继承自 Animal 类,但是却没有实现 Animal 类上的抽象方法 sayHi,报错。

正确的用法如下:

ts
abstract class Animal {
    constructor(name:string) {
        this.name = name;
    }
    public name: string
    public abstract sayHi():void
}

class Dog extends Animal {
    constructor(name:string) {
        super(name);
    }
    public sayHi() {
        console.log('wang');
    }
}

implements 关键字 ​

interface 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述。interface 同样可以用来约束 class,要实现约束,需要用到 implements 关键字。

比如手机有播放音乐的功能,可以这么写:

ts
interface MusicInterface {
    playMusic(): void
}

class Cellphone implements MusicInterface {
    playMusic() {}
}

定义了约束后,class 必须要满足接口上的所有条件。如果 Cellphone 类上不写 playMusic 方法,会报错:

ts
class Cellphone implements MusicInterface {
}
// Error: Class 'Cellphone' incorrectly implements interface 'MusicInterface'.
// Property 'playMusic' is missing in type 'Cellphone' but required in type 'MusicInterface'

处理公共的属性和方法 ​

不同的类有一些共同的属性和方法,使用继承很难完成。比如汽车(Car 类)也有播放音乐的功能,你可以这么做:

  • 用 Car 类继承 Cellphone 类
  • 找一个 Car 类和 Cellphone 类的父类,父类有播放音乐的方法,他们俩继承这个父类

很显然这两种方法都不合常理。使用 implements,问题就会迎刃而解:

ts
interface MusicInterface {
    playMusic(): void
}

class Car implements MusicInterface {
    playMusic() {}
}

class Cellphone implements MusicInterface {
    playMusic() {}
}

这样 Car 类和 Cellphone 类都约束了播放音乐的功能。

再比如,手机还有打电话的功能,就可以这么做,Cellphone 类 implements 两个 interface:

ts
interface MusicInterface {
    playMusic(): void
}

interface CallInterface {
    makePhoneCall(): void
}

class Cellphone implements MusicInterface, CallInterface {
    playMusic() {}
    makePhoneCall() {}
}

这个 CallInterface 也可以用于 iPad 类、手表类上面,毕竟他们也能打电话。

interface 来约束 class,只要 class 实现了 interface 规定的属性或方法,就行了,没有继承那么多条条框框,非常灵活。

约束构造函数和静态属性 ​

使用 implements 只能约束类实例上的属性和方法,要约束构造函数和静态属性,需要怎么写?以我们上文提过的 Circl 类为例:

ts
interface CircleStatic {
    new (radius: number): void
    pi: number
}

const Circle:CircleStatic = class Circle {
    static pi: 3.14
    public radius: number
    public constructor(radius: number) {
        this.radius = radius;
    }
}

未定义静态属性 pi,会报错:

ts
const Circle:CircleStatic = class Circle {
    public radius: number
    public constructor(radius: number) {
        this.radius = radius;
    }
}
// Error: Property 'pi' is missing in type 'typeof Circle' but required in type 'CircleStatic'.

constructor 入参类型不对,会报错:

ts
const Circle:CircleStatic = class Circle {
    static pi: 3.14
    public radius: number
    public constructor(radius: string) {
        this.radius = radius;
    }
}
// Error: Type 'typeof Circle' is not assignable to type 'CircleStatic'.
//   Types of parameters 'radius' and 'radius' are incompatible.
//      Type 'number' is not assignable to type 'string'.
// Type 'string' is not assignable to type 'number'.

类型推断 ​

TypeScript 能根据一些简单的规则推断(检查)变量的类型,这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

定义变量 ​

定义时不赋值,就会被 TS 自动推导成 any 类型,之后随便怎么赋值都不会报错:

ts
let a;

a = 18;
a = 'jimco';

若定义变量同时赋值,则类型由定义推断:

ts
let foo = 123;

foo = 'hello'; // Error: Type 'string' is not assignable to type 'number'.

因为赋值的时候赋的是一个数字,所以 TS 自动推导出 foo 是 number 类型。

默认参数值 ​

函数设置默认参数时,也会有自动推导。比如,定义一个打印年龄的函数,默认值是 18:

ts
function printAge(num = 18) {
    console.log(num);
    return num;
}

那么 TS 会自动推导出 printAge 的入参类型,传错了类型会报错。

ts
printAge('18');
// Error: Argument of type 'string' is not assignable to parameter of type 'number'.

函数返回值 ​

决定函数返回值时, TS 也会自动推导出返回值类型。比如一个函数不写返回值:

ts
function welcome() {
    console.log('hello');
}

TS 自动推导出返回值是 void 类型。

再比如上文的 printAge 函数,TS 会自动推导出返回值是 number 类型。如果我们给 printAge 函数的返回值定义为 string 类型,看看会发生什么:

ts
function printAge(num = 18) {
    console.log(num);
    return num;
}

interface PrintAge {
    (num: number): string
}

const printAge1: PrintAge = printAge;
// Error: Type '(num?: number) => number' is not assignable to type 'PrintAge'.
//   Type 'number' is not assignable to type 'string'.

很显然,定义的类型和 TS 自动推导出的类型冲突,报错。

结构化 ​

这些简单的规则也适用于结构化的存在(对象字面量),例如在下面这种情况下 foo 的类型被推断为 { a: number, b: number }:

ts
const foo = {
  a: 123,
  b: 456
};

foo.a = 'hello'; // Error: Type 'string' is not assignable to type 'number'.

数组也一样:

ts
const bar = [1, 2, 3];
bar[0] = 'hello'; // Error: Type 'string' is not assignable to type 'number'.

也适用于解构中:

ts
const foo = {
  a: 123,
  b: 456
};
let { a } = foo;

a = 'hello'; // Type 'string' is not assignable to type 'number'.

数组结构与函数参数解构也同样如此。

内置类型 ​

JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。

内置对象是指根据标准在全局作用域 global 上存在的对象,这里的标准指的是 ECMAcript 和其他环境(比如DOM)的标准。

JS 八种内置类型 ​

ts
let name: string = 'jimco';
let age: number = 18;
let isHandsome: boolean = true;
let u: undefined = undefined;
let n: null = null;
let obj: object = { name: 'jimco', age: 18 };
let big: bigint = 100n;
let sym: symbol = Symbol('jimco');

ECMAScript 的内置对象 ​

比如,Array、Date、Error 等:

ts
const nums: Array<number> = [1,2,3];
const date: Date = new Date();
const err: Error = new Error('Error!');
const reg: RegExp = /abc/;

Math.pow(2, 9);

以 Array 为例:

Array 类型定义

可以看到,Array 这个类型是用 interface 定义的,有多个不同版本的 .d.ts 文件声明了这个类型。

在 TS 中,重复声明一个 interface,会把所有的声明全部合并,这里所有的 .d.ts 文件合并出来的 Array 接口,就组合成了 Array 内置类型的全部属性和功能。

DOM 和 BOM ​

比如 HTMLElement、NodeList、MouseEvent 等:

ts
let body: HTMLElement = document.body
let allDiv: NodeList = document.querySelectorAll('div');

document.addEventListener('click', (e: MouseEvent) => {
    e.preventDefault()
    // Do something
});

核心库的定义文件 ​

TypeScript 核心库的定义文件中定义了所有浏览器环境需要用到的类型,并且是预置在 TypeScript 中的。比如 Math.pow 的类型定义如下:

ts
interface Math {
    /**
     * Returns the value of a base expression taken to a specified power.
     * @param x The base value of the expression.
     * @param y The exponent value of the expression.
     */
    pow(x: number, y: number): number;
}

又比如,addEventListener 的类型定义如下:

ts
interface Document extends Node, GlobalEventHandlers, NodeSelector, DocumentEvent {
    addEventListener(type: string, listener: (ev: MouseEvent) => any, useCapture?: boolean): void;
}

泛型 ​

泛型,是 TS 比较难理解的部分,拿下了泛型,对 TS 的理解就又上了一个台阶,对后续深入学习帮助很大。

为什么需要泛型 ​

先来看这样一个例子,体会一下泛型解决的问题。定义一个 print 函数,这个函数的功能是把传入的参数打印出来,再返回这个参数,传入参数的类型是 string,函数返回类型为 string:

ts
function print(arg: string): string {
    console.log(arg);
    return arg;
}

现在需求变了,我还需要打印 number 类型,怎么办?可以使用联合类型来改造:

ts
function print(arg: string | number): string | number {
    console.log(arg);
    return arg;
}

现在需求又变了,我还需要打印 string 数组、number 数组,甚至任何类型,怎么办?有个笨方法,支持多少类型就写多少联合类型,或者把参数类型改成 any:

ts
function print(arg: any): any {
    console.log(arg);
    return arg;
}

且不说写 any 类型不好,毕竟在 TS 中尽量不要写 any。而且这也不是我们想要的结果,只能说传入的值是 any 类型,输出的值是 any 类型,传入和返回并不是统一的。这么写甚至还会出现 bug:

ts
const res:string = print(123);

定义 string 类型来接收 print 函数的返回值,返回的是个 number 类型,TS 并不会报错提示我们。这个时候,泛型就出现了,它可以轻松解决输入输出要一致的问题

注意:泛型不是为了解决这一个问题设计出来的,泛型还解决了很多其他问题,这里是通过这个例子来引出泛型。

基本使用 ​

处理函数参数 ​

泛型的语法是 <> 里写类型参数,一般可以用 T 来表示:

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

这样,我们就做到了输入和输出的类型统一,且可以输入输出任何类型。如果类型不统一,就会报错:

ts
const res: string = print(123); // Error: Type 'number' is not assignable to type 'string'.

泛型中的 T 就像一个占位符、或者说一个变量,在使用的时候可以把定义的类型像参数一样传入,它可以原封不动地输出。

泛型的写法对前端工程师来说是有些古怪,比如 <>, T ,但记住就好,只要一看到 <>,就知道这是泛型。

我们在使用的时候可以有两种方式指定类型:

  • 定义要使用的类型
  • TS 类型推断,自动推导出类型
ts
print<string>('hello'); // 定义 T 为 string
print('hello');         // TS 类型推断,自动推导类型为 string

我们知道,type 和 interface 都可以定义函数类型,也用泛型来写一下,type 这么写:

ts
type Print = <T>(arg: T) => T;
const printFn: Print = function print(arg) {
    console.log(arg);
    return arg;
}

interface 这么写:

ts
interface Iprint<T> {
    (arg: T): T
}

function print<T>(arg: T) {
    console.log(arg);
    return arg;
}

const myPrint: Iprint<number> = print;

默认参数 ​

如果要给泛型加默认参数,可以这么写:

ts
interface Iprint<T = number> {
    (arg: T): T
}

function print<T>(arg: T) {
    console.log(arg);
    return arg;
}

const myPrint: Iprint = print

这样默认就是 number 类型了,怎么样,是不是感觉 T 就如同函数参数一样呢。

处理多个函数参数 ​

现在有这么一个函数,传入一个只有两项的元组,交换元组的第 0 项和第 1 项,返回这个元组:

ts
function swap(tuple) {
    return [tuple[1], tuple[0]];
}

这么写,我们就丧失了类型,用泛型来改造一下。我们用 T 代表第 0 项的类型,用 U 代表第 1 项的类型:

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

这样就可以实现了元组第 0 项和第 1 项类型的控制。

函数副作用操作 ​

泛型不仅可以很方便地约束函数的参数类型,还可以用在函数执行副作用操作的时候。比如我们有一个通用的异步请求方法,想根据不同的 url 请求返回不同类型的数据:

ts
function request(url: string) {
    return fetch(url).then(res => res.json());
}

调一个获取用户信息的接口:

ts
request('user/info').then(res =>{
    console.log(res);
});

这时候的返回结果 res 就是一个 any 类型,非常讨厌。我们希望调用 API 都清晰的知道返回类型是什么数据结构,就可以这么做:

ts
interface UserInfo {
    name: string
    age: number
}

function request<T>(url:string): Promise<T> {
    return fetch(url).then(res => res.json())
}

request<UserInfo>('user/info').then(res =>{
    console.log(res);
});

这样就能很舒服地拿到接口返回的数据类型,开发效率大大提高。

约束泛型 ​

假设现在有这么一个函数,打印传入参数的长度,我们这么写:

ts
function printLength<T>(arg: T): T {
    console.log(arg.length); // Error: Property 'length' does not exist on type 'T'.
    return arg;
}

因为不确定 T 是否有 length 属性,会报错。

那么现在我想约束这个泛型,一定要有 length 属性,怎么办?可以和 interface 结合,来约束类型:

ts
interface ILength {
    length: number
}

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

这其中的关键就是 <T extends ILength>,让这个泛型继承接口 ILength,这样就能约束泛型。

我们定义的变量一定要有 length 属性,比如下面的 str、arr 和 obj,才可以通过 TS 编译:

ts
const str = printLength('jimco');
const arr = printLength([1,2,3]);
const obj = printLength({ length: 10 });

这个例子也再次印证了 interface 的 Duck Typing。只要你有 length 属性,都符合约束,那就不管你是 str,arr 还是 obj,都没问题。

当然,我们定义一个不包含 length 属性的变量,比如数字,就会报错:

ts
const num = printLength(18);
// Error: Argument of type 'number' is not assignable to parameter of type 'ILength'.

泛型误用 ​

使用泛型不应仅仅是为了它的 hack。当你使用它时,应该问问自己,你想用它来提供什么样的约束。如果你不能很好的回答它,你可能会误用泛型,如:

ts
declare function foo<T>(arg: T): void;

在这里,泛型完全没有必要使用,因为它仅用于单个参数的位置,使用如下方式可能更好:

ts
declare function foo(arg: any): void;

泛型应用 ​

使用泛型,可以在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。

泛型约束类 ​

定义一个栈,有入栈和出栈两个方法,如果想入栈和出栈的元素类型统一,就可以这么写:

ts
class Stack<T> {
    private data: T[] = []
    push(item: T) {
        return this.data.push(item);
    }
    pop(): T | undefined {
        return this.data.pop();
    }
}

在定义实例的时候写类型,比如,入栈和出栈都要是 number 类型,就这么写:

ts
const s1 = new Stack<number>();

这样,入栈一个字符串就会报错:

ts
s1.push(18);
s1.push('jimco'); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

这是非常灵活的,如果需求变了,入栈和出栈都要是 string 类型,在定义实例的时候改一下就好了:

ts
const s1 = new Stack<string>();

这样,入栈一个数字就会报错:

ts
s1.push(18); // Error: Argument of type 'number' is not assignable to parameter of type 'string'.
s1.push('jimco');

特别注意的是,泛型无法约束类的静态成员。给 pop 方法定义 static 关键字,就报错了:

ts
class Stack<T> {
    private data: T[] = []
    push(item: T) {
        return this.data.push(item);
    }
    static pop(): T | undefined { // Error: Static members cannot reference class type parameters.
        return this.data.pop();
    }
}

泛型约束接口 ​

使用泛型,也可以对 interface 进行改造,让 interface 更灵活:

ts
interface IKeyValue<T, U> {
    key: T
    value: U
}

const k1: IKeyValue<number, string> = { key: 18, value: 'jimco' };
const k2: IKeyValue<string, number> = { key: 'jimco', value: 18 };

泛型定义数组 ​

定义一个数组,我们之前是这么写的:

ts
const arr: number[] = [1, 2, 3];

现在这么写也可以:

ts
const arr: Array<number> = [1, 2, 3];

数组项写错类型,报错:

ts
const arr: Array<number> = [1, 2, 'aaa']; // Error: Type 'string' is not assignable to type 'number'.

高级类型 ​

联合类型 ​

如果希望一个变量可以支持多种类型,就可以用联合类型(union types)来定义。例如,一个变量既支持 number 类型,又支持 string 类型,就可以这么写:

ts
let num: number | string;

num = 8;
num = 'eight';

联合类型大大提高了类型的可扩展性,但当 TS 不确定一个联合类型的变量到底是哪个类型的时候,只能访问他们共有的属性和方法。

比如下面的例子,num 就只能访问 number 类型和 string 类型共有的方法,如果直接访问 length 属性,string 类型上有,number 类型上没有,就报错了:

ts
let num: number | string;
console.log(num.length);
// Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

交叉类型 ​

联合类型 | 是指可以取几种类型中的任意一种,而交叉类型 & 是指把几种类型合并起来。交叉类型和 interface 的 extends 非常类似,如果要对对象形状进行扩展,可以使用交叉类型 &。

比如 Person 有 name 和 age 的属性,而 Student 在 name 和 age 的基础上还有 grade 属性,就可以这么写:

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

type Student = Person & { grade: number };

这和类的继承是一模一样的,这样 Student 就继承了 Person 上的属性。

类型别名 ​

类型别名会给一个类型起个新名字。 类型别名有时和 interface 很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。

类型别名用 type 关键字来书写,有了类型别名,我们书写 TS 的时候可以更加方便简洁。比如下面这个例子,getName 这个函数接收的参数可能是字符串,可能是函数,就可以这么写:

ts
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver; // 联合类型

function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

这样调用时传字符串和函数都可以:

ts
getName('jimco');
getName(() => 'jimco');

再看一个例子:

ts
type Name = string;             // 基本类型
type arrItem = number | string; // 联合类型
const arr: arrItem[] = [1, '2', 3];

type Person = {
    name: Name
};
type Student = Person & { grade: number  };       // 交叉类型
type Teacher = Person & { major: string  };
type StudentAndTeacherList = [Student, Teacher];  // 元组类型

const list:StudentAndTeacherList = [
    { name: 'jimco', grade: 100 },
    { name: 'jimco', major: 'Chinese' }
]

type 和 interface 的区别 ​

共同点:

  • 都可以定义一个对象或函数
  • 都允许继承

不同点:

  • interface 是 TS 设计出来用于定义对象类型的,可以对对象的形状进行描述
  • type 是类型别名,用于给各种类型定义别名,让 TS 写起来更简洁、清晰
  • type 可以声明基本类型、联合类型、交叉类型、元组,interface 不行
  • interface 可以合并重复声明,type 不行

比如下面这个例子,可以用 type,也可以用 interface:

ts
interface Person {
    name: string
    age: number
}

const person: Person = {
    name: 'jimco',
    age: 18
}
ts
type Person = {
    name: string
    age: number
}

const person: Person = {
    name: 'jimco',
    age: 18
}

都可以定义一个对象或函数:

ts
type addType = (num1:number, num2:number) => number

interface addType {
    (num1:number, num2:number): number
}
// 这两种写法都可以定义函数类型

都允许继承:

ts
// interface 继承 interface
interface Person {
    name: string
}

interface Student extends Person {
    grade: number
}
ts
// type 继承 type
type Person = {
    name: string
}

type Student = Person & { grade: number  }   // 用交叉类型
ts
// interface 继承 type
type Person = {
    name: string
}

interface Student extends Person {
    grade: number
}
ts
// type 继承 interface
interface Person {
  name: string
}

type Student = Person & { grade: number  }   // 用交叉类型

interface 使用 extends 实现继承,type 使用交叉类型实现继承。

合并重复声明:

ts
interface Person {
    name: string
}

// 重复声明 interface,就合并了
interface Person {
    age: number
}

const person: Person = {
    name: 'jimco',
    age: 18
}

重复声明 type,就报错了:

ts
type Person = {
    name: string
}

type Person = { // Error: Duplicate identifier 'Person'
    age: number
}

const person: Person = {
    name: 'jimco',
    age: 18
}

其实本不该把这两个东西拿来做对比,他们俩是完全不同的概念。interface 是接口,用于描述一个对象,type 是类型别名,用于给各种类型定义别名,让 TS 写起来更简洁、清晰,只是有时候两者都能实现同样的功能,才会经常被混淆。

平时开发中,一般使用组合或者交叉类型的时候,用 type,一般要用类的 extends 或 implements 时,用 interface,其他情况,比如定义一个对象或者函数,就看你心情了。

类型保护 ​

如果有一个 getLength 函数,入参是联合类型 number | string,返回入参的 length:

ts
function getLength(arg: number | string): number {
    return arg.length
}
// Error: Property 'length' does not exist on type 'string | number'.
//   Property 'length' does not exist on type 'number'.

从上文可知,这么写会报错,因为 number 类型上没有 length 属性。这个时候,类型保护(Type Guards)出现了,可以使用 typeof 关键字判断变量的类型。把 getLength 方法改造一下,就可以精准地获取到 string 类型的 length 属性了:

ts
function getLength(arg: number | string): number {
    if(typeof arg === 'string') {
        return arg.length;
    } else {
        return arg.toString().length;
    }
}

之所以叫类型保护,就是为了能够在不同的分支条件中缩小范围,这样我们代码出错的几率就大大降低了。

类型断言 ​

上文的例子也可以使用类型断言来解决。类型断言语法:值 as 类型。

使用类型断言来告诉 TS,我(开发者)比你(编译器)更清楚这个参数是什么类型,你就别给我报错了:

ts
function getLength(arg: number | string): number {
    const str = arg as string;
    if (str.length) {
        return str.length;
    } else {
        const number = arg as number;
        return number.toString().length;
    }
}

注意,类型断言不是类型转换,把一个类型断言成联合类型中不存在的类型会报错。比如:

ts
function getLength(arg: number | string): number {
    return (arg as number[]).length;
}
// Error: Conversion of type 'string | number' to type 'number[]' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
//   Type 'number' is not comparable to type 'number[]'.

字面量类型 ​

有时候,我们需要定义一些常量,就需要用到字面量类型。比如:

ts
type ButtonSize = 'mini' | 'small' | 'normal' | 'large';

type Sex = '男' | '女';

这样就只能从这些定义的常量中取值,乱取值会报错:

ts
const sex: Sex = '不男不女';
// Error: Type '"不男不女"' is not assignable to type 'Sex'.

索引类型 ​

从对象中抽取一些属性的值,然后拼接成数组,可以这么写:

ts
const userInfo = {
    name: 'jimco',
    age: '18',
}

function getValues(userInfo: any, keys: string[]) {
    return keys.map(key => userInfo[key]);
}

// 抽取指定属性的值
console.log(getValues(userInfo, ['name', 'age']));  // ['jimco', '18']
// 抽取 obj 中没有的属性:
console.log(getValues(userInfo, ['sex', 'outlook']));  // [undefined, undefined]

虽然 obj 中并不包含 sex 和 outlook 属性,但 TS 编译器并未报错,此时可以使用 TS 索引类型,对这种情况做类型约束,实现动态属性的检查。

理解索引类型,需先理解 keyof(索引查询)、T[K](索引访问)和 extends (泛型约束)。

keyof 索引查询 ​

keyof 操作符可以用于获取某种类型的所有键,其返回类型是联合类型:

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

type Test = keyof IPerson; // 'name' | 'age'

上面的例子,Test 类型变成了一个字符串字面量。

T[K] 索引访问 ​

T[K] 表示接口 T 的属性 K 所代表的类型:

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

let type1:  IPerson['name']; // string
let type2:  IPerson['age'];  // number

extends 泛型约束 ​

T extends U 表示泛型变量可以通过继承某个类型,获得某些属性:

ts
interface ILength {
    length: number
}

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

这样入参就一定要有 length 属性,比如 str、arr``、obj 都可以,num 就不行:

ts
const str = printLength('jimco');
const arr = printLength([1,2,3]);
const obj = printLength({ length: 10 });

const num = printLength(10); // Error: Argument of type 'number' is not assignable to parameter of type 'ILength'.

检查动态属性 ​

对索引类型的几个概念了解后,对 getValue 函数进行改造,实现对象上动态属性的检查。改造前:

js
const userInfo = {
    name: 'jimco',
    age: '18',
}

function getValues(userInfo: any, keys: string[]) {
    return keys.map(key => userInfo[key]);
}
  • 定义泛型 T、K,用于约束 userInfo 和 keys
  • 为 K 增加一个泛型约束,使 K 继承 userInfo 的所有属性的联合类型,即 K extends keyof T

改造后:

ts
function getValues<T, K extends keyof T>(userInfo: T, keys: K[]): T[K][] {
    return keys.map(key => userInfo[key]);
}

这样当我们指定不在对象里的属性时,就会报错:

ts
getValues(userInfo, ['sex', 'outlook']);
// Error: Type '"sex"' is not assignable to type '"name" | "age"'.
// Type '"outlook"' is not assignable to type '"name" | "age"'.

映射类型 ​

TS允许将一个类型映射成另外一个类型。

in ​

介绍映射类型之前,先介绍一下 in 操作符,用来对联合类型实现遍历:

ts
type Person = 'name' | 'school' | 'major';

type Obj =  {
    [p in Person]: string
}

in 操作符

Partial ​

Partial<T> 将 T 的所有属性映射为可选的,例如:

ts
interface IPerson {
    name: string
    age: number
}

let p1: IPerson = {
    name: 'jimco',
    age: 18
}

使用了 IPerson 接口,就一定要传 name 和 age 属性。使用 Partial 改造一下,就可以变成可选属性:

ts
interface IPerson {
    name: string
    age: number
}

type IPartial = Partial<IPerson>;

let p1: IPartial = {};

Partial 原理:

Partial 的实现用到了 in 和 keyof:

ts
/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P]
}
  • [P in keyof T] 遍历 T 上的所有属性
  • ?: 设置属性为可选的
  • T[P] 设置类型为原来的类型

Required ​

Required<T> 的作用是将传入的属性变为必选项,例如:

ts
interface IPerson {
    name: string
    age?: number
}

type IRequired = Required<IPerson>;

let p1: IRequired = { name: 'jimco', age: 18 };

Required 原理

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

这边有一个有意思的用法 -?,很好理解,就是将表示可选项的 ? 去掉,从而让这个类型变成必选项。与之对应的还有个 +?,这个含义自然与 -? 相反,它是用来把属性变成可选项的。举个例子:

ts
type Mutable<T> = {
    -readonly [P in keyof T]: T[P]
}

以下代码的作用就是将 T 的所有属性的 readonly 移除,使之成为可修改属性。

Readonly ​

Readonly<T> 将 T 的所有属性映射为只读的,例如:

ts
interface IPerson {
    name: string
    age: number
}

type IReadOnly = Readonly<IPerson>

let p1: IReadOnly = {
    name: 'jimco',
    age: 18
}

p1.name = 'ddd'; // Error: Cannot assign to 'name' because it is a read-only property.

Readonly 原理

和 Partial 几乎完全一样:

ts
/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P]
}
  • [P in keyof T] 遍历 T 上的所有属性
  • readonly 设置属性为只读的
  • T[P] 设置类型为原来的类型

Pick ​

Pick 用于抽取对象子集,挑选一组属性并组成一个新的类型,例如:

ts
interface IPerson {
    name: string
    age: number
    sex: string
}

type IPick = Pick<IPerson, 'name' | 'age'>

let p1: IPick = {
    name: 'jimco',
    age: 18
}

这样就把 name 和 age 从 IPerson 中抽取出来。

Pick 原理

ts
/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P]
}

Pick映射类型有两个参数:

  • 第一个参数 T,表示要抽取的目标对象
  • 第二个参数 K,具有一个约束:K 一定要来自 T 所有属性字面量的联合类型

Record ​

上面三种映射类型官方称为同态,意思是只作用于对象属性而不会引入新的属性。Record 是会创建新属性的非同态映射类型:

ts
interface IPerson {
    name: string
    age: number
}

type IRecord = Record<string, IPerson>

let personMap: IRecord = {
    person1: {
        name: 'jimco',
        age: 18
    },
    person2: {
        name: 'lily',
        age: 25
    }
}

Record 原理

ts
/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T
}

Record 映射类型有两个参数:

  • 第一个参数可以传入继承于 any 的任何值
  • 第二个参数,作为新创建对象的值,被传入

条件类型 ​

ts
T extends U ? X : Y
//若类型 T 可被赋值给类型 U,那么结果类型就是 X 类型,否则就是 Y 类型

Exclude 和 Extract 的实现就用到了条件类型。

Exclude ​

Exclude 意思是不包含,Exclude<T, U> 会返回 联合类型 T 中不包含 联合类型 U 的部分。

ts
type Test = Exclude<'a' | 'b' | 'c', 'a'>

Exclude 原理

ts
/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T
  • never 表示一个不存在的类型
  • never 与其他类型的联合后,为其他类型
ts
type Test = string | number | never

Extract ​

Extract<T, U> 提取联合类型 T 和联合类型 U 的所有交集。

ts
type Test = Extract<'key1' | 'key2', 'key1'>

Extract 原理

ts
/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never

懂了 Exclude,也就懂了 Extract。

工具类型 ​

为了方便开发者使用, TypeScript 内置了一些常用的工具类型。上面我们介绍的索引类型、映射类型和条件类型都是工具类型。除了这些,还有一些常用的工具函数。

Omit ​

Omit<T, U> 从类型 T 中剔除 U 中的所有属性。

ts
interface IPerson {
    name: string
    age: number
}

type IOmit = Omit<IPerson, 'age'>

这样就剔除了 IPerson 上的 age 属性。

Omit 原理

ts
/**
 * Construct a type with the properties of T except for those in type K.
 */
type IOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

Pick 用于挑选一组属性并组成一个新的类型,Omit 是剔除一些属性,留下剩余的,他们俩有点相反的感觉。那么就可以用 Pick 和 Exclude 实现 Omit。

当然也可以不用 Pick 实现:

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

NonNullable ​

NonNullable<T> 用来过滤类型中的 null 及 undefined 类型:

ts
type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

NonNullable 原理

ts
/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T
  • never 表示一个不存在的类型
  • never 与其他类型的联合后,为其他类型

Parameters ​

Parameters 获取函数的参数类型,将每个参数类型放在一个元组中:

ts
type T1 = Parameters<() => string>  // []
type T2 = Parameters<(arg: string) => void>  // [string]
type T3 = Parameters<(arg1: string, arg2: number) => void> // [arg1: string, arg2: number]

Parameters 原理

ts
/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

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

  • Parameters 首先约束参数 T 必须是个函数类型
  • 判断 T 是否是函数类型,如果是则使用 infer P 暂时存一下函数的参数类型,后面的语句直接用 P 即可得到这个类型并返回,否则就返回 never。

ReturnType ​

ReturnType 获取函数的返回值类型:

ts
type T0 = ReturnType<() => string>  // string

type T1 = ReturnType<(s: string) => void>  // void

ReturnType 原理

ts
/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

懂了 Parameters,也就懂了 ReturnType:

  • ReturnType 首先约束参数 T 必须是个函数类型
  • 判断 T 是否是函数类型,如果是则使用 infer R 暂时存一下函数的返回值类型,后面的语句直接用 R 即可得到这个类型并返回,否则就返回 any

参考资料 ​