Typescript学习笔记


在 TypeScript 中,可以把类型理解成一系列值的集合,例如:

number:所有数字的集合
对象类型:内部所有属性的集合

顶层类型与底层类型

TypeScript 有两个顶层类型anyunknown),一个底层类型never)。

  • 顶层类型:所有类型的并集,也可以说是所有类型的父集
  • 底层类型:与顶层类型相反,可以说是所有类型的子集

类型兼容

TypeScript 中,当类型 A 在集合上属于类型 B 的父集时,他就可以赋值给类型 B,而反过来则会报错。

例如对于上文的顶层与底层类型,可以追加两个定义:

  • 顶层类型: 任何类型都可以赋值给顶层类型,但顶层类型不能赋值给其他类型
  • 底层类型: 与顶层类型相反,它可以赋值给任意类型,但其他类型都不能赋值给它

any

any 属于 TypeScript 的顶层类型,所以任何类型都可以赋值给他。

1
let a: any = 10;

但 any 有个很在 TypeScript 中独一无二的地方,即它也可以被赋值给任意类型(神一样的存在)

1
let b: number = <any>"10"; // 不会报错

听我一句劝,不要写 AnyScript

自动推断

当一个类型,TypeScript 无法推断出他的具体类型时,就会被标记为 any
相对应的,TypeScript 提供了一个编译选项: noImplicitAny,开启该选项时,只要自动推断出 any,就会报错。

1
2
3
4
5
6
// tsconfig.json
{
"compilerOptions": {
"noImplicitAny": true
}
}

但有一个特例,对于使用 letvar 定义的未设置初始值的变量,未对其设置类型时,会被自动推断出 any,但此时不会报错。

1
2
let a; // 不会报错
var b; // 不会报错

因为这个原因,建议在使用 let/var 定义变量时,如果没有初始值,一定要手动为其设置类型。

unknown

该类型代表任何值,属于严格意义上的 TypeScript 的顶层类型

1
let a: unknown = 10;

与 any 的不同点

unknown 不允许赋值给除 any 与自身的任何类型

1
let a: number = <unknown>10; // 报错

类型为 unknown 的方法,不能直接调用

1
2
let foo: unknown = () => {};
foo(); // 报错

unknown 只能作有限的运算,分别为比较运算(===!==||&&?)、取反(?)、typeofinstanceof

1
2
3
let a: unknown = 10;
a + 1; // 报错
a === 10; // 不会报错

当使用 keyof 关键字对其进行类型提取时,与 any 的表现也不一样(keyof 关键字后文会讲):

1
2
let any: keyof any; // any: string | number | symbol
let unknown: keyof unknown; // unknown: never

使用方式

unknown 的最佳使用方式是通过类型缩小来确定类型

1
2
3
4
5
let a: unknown = 10;

if ( typeof a === "number" ) {
a; // a: number
}

基本所有范围较大的类型的最终使用方式都是类型缩小,包括后面要提到的 object、联合类型等

never

从集合论上讲,never 属于空集。它是所有类型的子集,是 TypeScript 唯一的底层类型

因为是所有类型的子集,所以它可以被赋值给任意类型

1
let a: number = <never>10;

数据类型

TypeScript 继承了 JavaScript 的类型设计,除了自身新增的类型外,JavaScript 中的类型 TypeScript 也全部支持。

基本类型

与 JavaScript 相同,即:numberstringbooleanbigintsymbolnullundefinedobject

下面仅针对部分较为特殊的做出说明。

boolean

boolean 类型其实就是 truefalse 两个字面量类型的集合。

object

object 是对象、数组、函数的集合,但其只定义了通用对象的属性,不包括用户自定义的对象属性

1
2
3
4
const obj: object = { a: 1 };

obj.toString; // 正确
obj.a; // 报错: property `a` does not exist on type `object`

null 与 undefined

编译配置项 strictNullChecks 可以对 nullundefined 做出更严格的校验。当它设置为 false 时,存在以下情况:

  • 对于使用 const 声明的未设置类型的变量,被赋值为 nullundefined 时,该变量的类型会被自动推断为 any
  • nullundefined 会像 any 那样可以被赋值给任意类型,他俩也可以被互相赋值

为了避免出现问题,建议还是将 strictNullChecks 打开比较好。

包装对象类型

JavaScript 中的五个原始类型均有其对应的包装对象,为此,TypeScript 也提供了对应的包装类型,这些包装类型均为基本类型的大写方式:

  • Boolean <–> boolean
  • Number <–> number
  • String <–> string
  • BigInt <–> bigint
  • Symbol <–> symbol

通过以下示例可以明确的看出包装类型与基本类型的区别

1
2
let str = "mari"; // str: string
let strObj = new String( "mari" ); // strObj: String

其中由于 BigInt()Symbol() 不允许作为构造函数使用,我们无法通过比较直接的方式得到包装类型。而且这两个类型的包装类型和基本类型在 TypeScript 中的没有区别,建议都使用小写的基本类型。

Object 类型

相比 objectObject 表示的是一种广义上的对象类型,只要可以转换为对象的值(除了 nullundefined),都属于 Object 类型,也就都可以赋值给 Object 类型。

{}Object 类型的简写方式,下面的写法是等价的

1
2
let obj1: Object;
let obj2: {};

值类型

在 TypeScript 中,单个原始类型值也是一个类型

1
2
3
4
let str: "mari";
let num: 18;
let bigNum: 18n;
let arr: [1, 2, 3];

值类型是一种非常底层的类型,在集合概念中为最小的集合。因此像下面这种赋值是都不被允许的

1
2
3
4
let str: "mari" = "mari";
let str2: string = "10";

str = str2; // 报错: Type `string` is not assignable to type `"mari"`

值类型与 const

使用 const 声明的原始类型值,会被自动推断为值类型。

1
2
3
const str = "mari"; // str: "mari"
const num = 18; // num: 18
const bigNum = 18n; // bigNum: 18n

表达式右边的结果不是一个确切的值时,不会推断为值类型

1
const num = 10 + 8; // num: number

很好理解,TypeScript 只编译,并不会进行语句执行,也就无法得到计算的结果。
这样就会导致出现一些奇怪的问题:

1
const num: 18 = 10 + 8; // 报错: Type `number` is not assignable to type `18`

联合类型

可以将多个类型组合成一个类型,每个类型使用 | 分割,这种新组成的类型叫做联合类型。联合类型可以理解成类型系统中的并集

1
2
3
4
5
// number | string 就是一个联合类型,表示既可以是字符串也可以是数字
let sex: number | string;
// 用对应的类型赋值均不会报错
sex = 0;
sex = "Armed helicopter";

联合类型可以由任意类型组成,数量不限

1
2
let name: "mari" | "marry" | "Marry Kozakura";
let height: number | string | null = null;

第一个类型前面也可以添加 |,这是为了便于排版书写

1
2
3
4
let name:
| "mari"
| "marry"
| "Marry Kozakura";

子类型收敛

若组成联合类型中的类型存在父子关系,则结果中仅会存在父类型。例如下面示例的 "mari" 类型就被 string 给“吃掉了”。

1
let name: "mari" | string | null; // name: string | null

anyunknown 两个顶层类型联合时,会得到 any 类型,可以勉强按照 any 范围比 unknown 更大来理解(虽然这样不准确)。

交叉类型

通过 & 拼接,多个类型也可以组成一个交叉类型。交叉类型即是多个类型的共同点,可以理解为类型系统中的交集

1
2
let name: string & number; // name: never
let value: ( string | number ) & ( number | boolean ); // value: number

上面的 name 示例中,由于 stringnumber 并不存在交集,所以结果为空集合 never 类型。

父类型收敛

与联合类型类似,交叉类型也存在类型收敛,结果中会仅保留子类型,父类型会被丢弃。

1
2
3
let val1: 1 & number; // val1: 1
let val2: "1" & string; // val2: "1"
let val3: true & boolean; // val3: true

特殊情况: 除了 never 类型外, any 类型与任何类型交叉均为 any 类型

1
2
let val1: number & any; // val1: any
let val2: any & never; // val2: never

对象类型的交叉

因为需要使得交叉结果均可继承自交叉双方的类型,所以当对象交叉时,结果是这样的

1
let obj: { name: string } & { age: number }; // obj: { name: string, age: number }

当两个对象类型存在相同属性但类型不同时,将会对该属性进行交叉

1
2
3
4
5
6
7
8
type Base1 = {
label: string
}
interface Base2 {
label: number
}
// 由于并不存在同事为 string 和 number 的类型,因此 label 的类型变为 never
let obj: { label: string } & { label: number }; // obj: { label: never }

函数类型的交叉

TypeScript 会利用函数重载的特性来实现不同函数的交叉运算

因此可能会出现如下这种情况

1
2
3
4
5
6
7
8
9
10
11
12
type F1 = (arg1: string, arg2: string) => void
type F2 = (arg1: number, arg2: number) => void
type F3 = F1 & F2
const fn: F3 = (a, b) => {}
/**
* 没有与此调用匹配的重载。
第 1 个重载(共 2 个),“(arg1: string, arg2: string): void”,出现以下错误。
类型“number”的参数不能赋给类型“string”的参数。
第 2 个重载(共 2 个),“(arg1: number, arg2: number): void”,出现以下错误。
类型“string”的参数不能赋给类型“number”的参数。ts(2769)
*/
fn('baka', 9)

且函数类型交叉运算后,相同参数变为联合类型,而返回值类型将做交叉运算

因此将会出现下面这种情况

1
2
3
4
5
6
7
8
type F1 = (arg1: string, arg2: string) => string
type F2 = (arg1: number, arg2: number) => number
type F3 = F1 & F2

const fn: F3 = (a, b) => {
// 必须将返回值断言为 never,否则报错
return '1' as never
}

数组类型

TypeScript 中有两种定义数组类型的方式,例如定义成员全部为 number 类型的数组

1
2
let arr1: Array<number>;
let arr2: number[];

从定义方式上可以看出,TypeScript 中,数组成员必须全部都为同一类型。若希望成员类型不同,则需要借助联合类型

1
2
let arr1: (number | string)[];
let arr2: Array<number | string>;

至于数组的长度,TypeScript 并不会对其进行限制,这就代表着数组成员可以动态变化,TypeScript 并不会对数组的边界进行检测。
这就导致了一个问题,在直接获取数组成员时,可能会得到错误的类型结果

1
2
const arr: number[] = [];
const firstEl = arr[0]; // firstEl: number

上面 firstEl 的类型被推断为 number,但很明显,它的结果为 undefined

获取成员类型

TypeScript 允许使用类似对象属性访问的方式获取指定成员的类型

1
let el: Array<bigint>[0]; // el: bigint

而我们知道,数组的键值都是 number 类型。因此使用下面的方式可以得到同样的效果

1
let el: Array<bigint>[number]; // el: bigint

开发中建议使用后者这种方式。

类型推断

在未显示标注类型时,TypeScript 会对数组类型进行推断,此时结果会根据数组的初始值的不同而有所不同。

1
2
3
const arr1 = []; // arr1: any[]
const arr2 = [ 1, 2, 3 ]; // arr2: number[]
const arr3 = [ 1, "1", true ]; // arr3: (string | number | boolean)[]

初始值为空数组的特殊情况

此时虽然初始值被自动推断为 any,但在后续更新数组内容时,它的类型将会被自动更新。

1
2
3
4
5
6
const arr = [];
arr; // arr: any[]
arr.push( 1 );
arr; // arr: number[]
arr.push( "1" );
arr; // arr: (string | number)[]

尽管自动更新了类型,但在添加时并不会进行阻拦。比如上面示例中,在 arr.push( "1" )arr 的类型已经变成了 number[],但依旧能正常添加字符串元素,并不会报错。

而如果我们在一个崭新的目录里编写上面的案例,反而会得到这个结果:

1
const arr = []; // arr: never[]

经过尝试后,得到在两种情况下会出现这种现象:

  • 当前项目结构下不存在 tsconfig.json
  • stricttrue 且同时 noImplicitAnyfalse

noImplicitAny 明明是一个约束 any 的配置,却在这种特定的组合下起了反作用。

官方在当年的 issue 里将这种现象描述为是这两个配置项的特定组合导致的意外结果,修复难度较大不打算修复。
目前应该也是延续到现在了,注意一下这种情况即可。

只读数组

正常的数组类型是可以随便更改数组成员的,包括成员值与成员数量。

但如果我们确实想定义一个不发生变更的常量数组,可以使用 readonly type[] 只读数组类型

1
2
3
4
const CONST_ARR: readonly number[] = [1, 2, 3];

CONST_ARR[1] = 2; // 报错: Index signature in type `readonly number[]` only permits reading.
CONST_ARR.push( 4 ); // 报错: Property 'push' does not exist on type 'readonly number[]'.

readonly number[]number[] 并不是同一个类型,前者在 TypeScript 中为后者的子类型。
所以不难理解上面示例的表现行为,即只读数组类型上并不存在 push 属性。

既然是父子类型,那么这俩同样遵守类型兼容中的规则,因此在需要数组类型的环境时,使用只读数组类型会出现错误

还需要注意的一点,只读数组类型不能使用数组的泛型写法,会报错

1
const CONST_ARR: readonly Array<number> = [1, 2, 3]; // 报错: `readonly` type modifier is only permitted on array and tuple literal types.

只读数组的另一种定义方式,是使用 const 断言,这回将变量断言为只读元组类型

1
const CONST_ARR = <const>[ 1, 2 ,3 ]; // CONST_ARR: readonly [1, 2, 3]

元组

元组是 TypeScript 中特有的类型,JavaScript 中并没有明确定义这种类型,其表示一个成员类型可以自由设置的数组。
所以元组在定义时,必须要明确声明每个成员的类型

1
const normal: [number, string, boolean] = [9, "baka", true];

元组的元素类型可以添加 ? 修饰符,表示这个类型是可选的。可选类型必须被放置在最后

1
2
const noraml: [number, string, boolean?] = [9, "baka"];
let normal1: [number, string?, boolean]; // 报错: A required element cannot follow an optional element.

自定义类型

通过接口与类型别名,可以自定义自己的类型

接口

类型别名

使用 type 关键字来自定义一个类型

1
type Name = string;

通常建议类型别名以大驼峰格式命名,方便与变量名区分。

块级作用域

类型别名、接口、枚举均拥有块级作用域,即:在块作用域内定义这些类型时,作用域以外的地方无法使用。

1
2
3
4
5
6
7
8
{
type Name = "mari" | "marry";
interface IPerson {
name: Name;
}
}
let pName: Name; // 报错: Cannot find name `Name`
let p: IPerson; // 报错: Cannot find name `IPerson`

枚举

运算符

typeof

提取一个变量的类型,只能在类型系统中使用,不能参与值运算说多少遍了,TypeScript 不参与运行时

它和 JavaScript 里的 typeof 长得一样但不是一个东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 解析值类型
const str = "mari";
type Str = typeof str; // Str: "mari"

// 解析函数
function normal(arg: string): boolean {
return !!arg;
}
type Normal = typeof normal; // Normal: (arg: string) => boolean

// 解析对象
const obj = {
label: "baka",
value: 9,
};
type Obj = typeof obj; // Obj: { label: string, value: number }

字面量类型/单元类型

typescript 允许模拟定义具体值类型

1
2
3
4
5
// 限制str的值,使其不允许被修改(模拟const)
let str: "mari" = "mari";
// 限制只能是几个值之一
let str1: "red" | "blue" | "green" = "red";
let str2: "color" | true | 1;

一般情况下, typescript 默认认为对象的属性是可以改变的,但在如下情况会出现问题

1
2
3
4
function handle(method: "GET" | "POST") {}
const obj = { method: "GET" };
// 报错:类型“string”的参数不能赋给类型“"GET" | "POST"”的参数。
handle(obj.method);

因为默认推断了 obj.methodstring 类型,然而 string 类型是有可能不属于 'GET' | 'POST' 类型的,因此报错
解决方法如下

1
2
3
4
// 将obj内属性设置为只读
const obj1 = { method: "GET" } as const;
// 或另一种断言方式
const obj2 = <const>{ method: "GET" };

bigint

指非常大的整数,使用时需将 target 设置为 es2020

1
const bigintNum: bigint = 100n;

never

never 指为不存在的类型,永远不会被观察到的值,它是所有类型的子类型,用集合来解释即:never 是空集

例如下情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Circle = {
kind: "circle";
};

type Square = {
kind: "square";
};

type Sharp = Circle | Square;

function sharpHandle(sharp: Sharp) {
switch (sharp.kind) {
case "circle":
// sharp: Circle;
return sharp;
case "square":
// sharp: Square;
return sharp;
default:
// sharp: never;
return sharp;
}
}

当一个函数抛出一个异常、中止程序执行 或 死循环,返回值的类型就是 never ,也用于联合类型没有匹配到任何东西的情况下的值类型

void

在 ts 中,一个不返回任何值的函数将自动推断返回值为 void 类型
而在 js 中,这种函数的返回值默认为 undefined

ts 中的 voidundefined 是不一样的

object

该类型指的是任何不是基元的值,其并不同于 {} ,也不同于 Object

基元: string number bigint boolean symbol null undefined

Function

注意大写,该类型描述了诸如 bind call apply 以及其他存在于 js 中所有函数值的属性。

Function 类型的值总是可以被调用,调用的方法均返回 any 。因此,并不推荐使用 Function 去定义函数类型。

函数

定义方法

类型别名和接口定义函数的方法分别为

1
2
3
4
5
6
/* 接口 */
interface Handle {
(arg: number): boolean;
}
/* 类型别名 */
type Handle = (arg: number) => boolean;

注:在函数类型的形参中,ts 仅限制了形参的最大数量以及每个形参的类型,对形参的名字无限制,允许定义的形参少于类型中形参的个数

1
2
3
4
type Handle = (arg1: number, arg2: booleab) => boolean;
// 少定义了一个参数,不会报错
// 等号右侧不需要明显定义类型其实,ts会自动推断
const handle: Handle = (mari: number): boolean => true;

函数签名

函数允许自带属性,类型定义方式如下

1
2
3
4
5
6
7
8
type Handle = {
desc: string;
(arg: number): boolean;
};

const handle: Handle = mari => mari === 1;
// 不定义会报错
handle.desc = "";

调用签名

即为函数的类型,如下表示:此函数有一个 string 类型的参数,无返回值的。且存在一个类型为 string 的属性 desc

1
2
3
4
5
6
7
8
interface IFunc {
(mari: string): boolean;
desc: string;
}

const handle: IFunc = (arg: string) => !!arg;
// 没有下面这一行会报错,因为desc为必须的属性
handle.desc = "函数的属性";

构造签名

即为 class 的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TsClass {
private arg: string;
constructor(arg: string) {
this.arg = arg;
}
}

interface ITsc {
new (mari: string): TsClass
}

// 或使用类型别名
// type ITsc = new (mari: string) => TsClass

function getTsClass(tsc: ITsc) {
return new tsc( "mari" );
}

若将形参 tsc 的类型修改为 TsClass ,则会出现以下错误

1
2
3
4
function getTsClass(tsc: TsClass) {
// 类型 "TsClass" 没有构造签名
return new tsc( "mari" );
}

可选参数

不建议在回调函数中定义可选参数

函数重载

提供 {} 表示对函数的实现,这里称其为为 实现函数,反之为 重载函数

实现函数虽然定义了参数可选,但那是为了兼容两个重载函数,调用时仍然只能使用两种重载的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function overload(timeStamp: number): Date;
function overload(d: number, m: number, y: number): Date;

/* 函数实现 */
function overload(dOrTimeStamp: number, m?: number, y?: number): Date {
if (m && y) {
return new Date(y, m, dOrTimeStamp);
} else {
return new Date(dOrTimeStamp);
}
}

overload(1650819030751);
overload(25, 4, 2022);
// 报错:没有需要 2 参数的重载,但存在需要 1 或 3 参数的重载。
overload(25, 4);

调用函数时,是看不到实现函数的参数的,只能看到重载函数的参数

1
2
3
4
function overload(field: string): void;
// 不会报错
function overload() {}
overload("this is a field");

实现函数若存在参数,则必须要兼容所有的重载函数的参数类型,返回值同样需要兼容

1
2
3
4
5
6
7
8
function overload(field: string): string;
function overload(field: number): number;

function overload(field: string | number): string | number {
return "field";
}

overload("this is a field");

可以的话,更推荐用 联合类型的参数 而不是重载参数,因为 重载参数 不能使用两者均有可能的值作为参数。

1
2
3
4
5
6
7
function overload(field: string): void;
function overload(field: number): void;

const field: string | number = Math.random() > 0.5 ? "" : 0;
// 类型“string | number”的参数不能赋给类型“string”的参数。
// 类型“string | number”的参数不能赋给类型“number”的参数。
overload(field);

this 入参

js 中不支持 this 作为函数参数,但 ts 允许这样做。
ts 允许在函数入参列表中第一个位置处,手动写入this并标记其类型。它仅仅是一个形式上的参数,并不会在编译后的代码中出现,仅仅是为了提供给 Typescript 编译器作为检查静态类型使用。

通常我们会在需要纠正 this 类型的场合下使用这个机制,参考如下示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Handle {
public value: string;
constructor(value: string) {
this.value = value
}

onClick(this: Handle, e: Event) {
this.value = e.type;
}
}

interface myElement {
addClickListener(onClick: (this: void, e: Event) => void): void
};

const ele: myElement = {
addClickListener(onClick: (this: void, e: Event) => void) {
onClick({ type: 'click' } as Event)
}
};

// TS2345: Argument of type "(this: Handle, e: Event) => void" is not assignable to parameter of type "(this: void, e: Event) => void".
ele.addClickListener(new Handle("default").onClick);

由于众所周知的 this 指向问题,上面代码中在调用 onClick 时,this 指向 undefined,而在 onClick 函数中期望的 this 则是指向 Handle 类。经过限制后 ts 捕获到了错误并作出了提醒。若不做限制(两处有任意一处未对 this 进行定义),则会出现 Cannot set property 'value' of undefined 报错。

参数展开

ts 可以给 es6 中的函数形参展开式定义类型

1
2
3
4
5
6
// 此处 m 若不做具体类型定义,则为 any[] 类型
function mutiply(n: string, ...m: number[]) {
return m.map((x) => n.padEnd(x, n));
}

mutiply( "mari", 6, 7, 8, 9, 10 );

同理,也可以展开数组实参传入方法,如下:

1
2
3
const list: string[] = [];
const list2: string[] = ["a", "b"];
list.push(...list2);

参数解构

1
2
3
4
5
6
7
8
9
10
type Parm = {
label: string;
value: number;
};

function handle({ label, value }: Parm) {
return `${label}: ${value}`;
}

handle({ label: "baka", value: 9 });

函数的 void 返回类型

当把函数类型定义为 返回 void 的函数 时,函数内允许返回任意类型,ts 会将其忽略,执行函数得到的结果被 ts 判断为 void 类型

1
2
3
4
5
6
7
8
type VoidFun = () => void;

// 不会报错
const v1: VoidFun = () => true;
// const res: void
const res = v1();
// true
console.log(res);

但若直接显式的给函数定义返回值类型为 void ,那么这个函数必须不能返回任何值

1
2
// 不能将类型“boolean”分配给类型“void”。
const v2 = (): void => true;

对象

定义方法

常见有下面几种定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 匿名
const obj: { label: string; value: number } = {
label: "baka",
value: 9,
};

// 接口
interface INormal {
label: string;
value: number;
}

const obj: INormal = {
label: "baka",
value: 9,
};

// 类型别名
type Normal = {
label: string;
value: number;
};

const obj: Normal = {
label: "baka",
value: 9,
};

属性修饰符

可选属性

1
2
3
4
interface INormal {
label?: string;
value: number;
}

只读属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface INormal {
readonly label: string;
value: number;
}

// ps: 如下这种情况并不会阻止修改 value 内部 title 或 count 的修改
interface INormal {
readonly value: {
title: string;
count: number;
};
}

obj.value.count = 99; // 不会报错

索引签名

格式为 { [key: KeyType]: ValueType }

其中 KeyType 仅能使用 numberstringsymbol模板字符串类型(类型章节有讲)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 基本使用
interface INormal {
[key: string]: number;
}

// 携带其他参数,必须和索引签名指定类型兼容
interface INormal {
[key: string]: number;
title: number;
}

// 只读属性
interface INormal {
readonly [key: string]: number;
}

Record 区别,Record 的 key 支持使用 字符串字面量类型与联合类型

类型扩展

接口

使用 extends 关键字对接口进行扩展,在后面继承部分会详细说明

接口和类型别名可以互相继承

1
2
3
4
5
6
7
8
9
// 继承单个
interface c extends a {
sex: string;
}

// 继承多个
interface d extends a, b {
address: string;
}

类型别名

使用交叉类型对类型别名进行扩展,交叉类型会在后面详细说明

接口和类型别名可以互相交叉

1
2
3
4
5
6
7
8
type c = a & {
sex: string;
};

type d = a &
b & {
address: string;
};

向现有类型添加字段

类型别名一经创建无法更改,无法添加字段

1
2
3
4
5
6
7
/* 接口 */
interface a {
name: string;
}
interface a {
age: number;
}

类型推断

三元表达式

ts 会根据三元表达式冒号两侧自动推断类型

1
2
// string: number | string
const string = Math.radom() > 0.5 ? 10 : "string";

函数返回值

变量经过函数内处理后返回,会自动进行类型缩小

1
2
3
4
5
6
function handle() {
let x: number | string | boolean;
x = Math.random() < 0.5 ? 10 : "string";
// x: string | number
return x;
}

类型断言

两种断言方法

1
2
let str = "string" as any;
let str = <any>"string";

typescript 只允许类型断言转换为更具体不太具体的类型,如不允许将 number 类型断言为 string

当不知道类型时,断言为 unknown 语义上比 any 更确切

类型谓词 is

对函数返回值定义类型 fieldName is Type ,当函数返回 true 时,变量会被指定为对应的类型
若不使用 is ,函数会被认为返回值类型是简单的 boolean ,变量依然是联合类型,无法确切定义具体类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type bird = {
fly: () => void;
};

type fish = {
swim: () => void;
};

/* 使用类型谓词is */
function isBird(pet: bird | fish): pet is bird {
return (<bird>pet).fly !== undefined;
}

function getPet(): bird | fish {
const fish: fish = {
swim: () => {},
};
const bird: bird = {
fly: () => {},
};
return Math.random() < 0.5 ? fish : bird;
}

const pet = getPet();

// typescript会自动推断不同条件下,pet的类型
isBird(pet) ? pet.fly() : pet.swim();

也可以用来过滤数组

1
2
3
4
5
const zoos: Array<fish | bird> = [getPet(), getPet(), getPet()];
// fishs: fish[]
const fishs = zoos.filter(isFish);
// fishs1: fish[]
const fishs1 = zoos.filter((pet): pet is fish => isFish(pet));

关键字

keyof

提取一个类型的所有键值组成一个新的类型

1
2
3
4
5
6
7
type Normal = {
label: string;
value: number;
};

// "label" | "value"
type KeysNormal = keyof Normal;

示例:获取一个类型的所有值的类型

1
2
3
4
5
6
type Normal = {
label: string
value: number
}
// type Values = string | number
type Values = Normal[keyof Normal]

示例:为获取对象属性的方法定义类型

1
2
3
function getProperty<T extends object, K extends keyof T>(obj: T, key: K): T[keyof T] {
return obj[key]
}

keyof 用于基本类型时,将会提取该类型的所有可用方法

1
2
3
4
// type Num = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
type Num = keyof number
// type Bool = "valueOf"
type Bool = keyof boolean

keyof 也可以用于 class,此时不会获取 class 中的 privateprotected 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
private label: string
public value: number
constructor(label: string, value: number) {
this.label = label
this.value = value
}
getValue() {
return this.value
}
}
// type Normal = "value" | "getValue"
type Normal = keyof Base

keyof 可以获取枚举类型,但直接使用会获得枚举属性的方法,想获取她的键值,需要结合 typeof 使用

1
2
3
4
5
6
7
8
enum HttpMethod {
Get,
Post
}
// type FakeMethod = "toString" | "toFixed" | "toExponential" | "toPrecision" | "valueOf" | "toLocaleString"
type FakeMethod = keyof HttpMethod
// type Method = "Get" | "Post"
type Method = keyof typeof HttpMethod

对索引签名使用

1
2
3
4
5
6
7
interface normal {
[fieldName: string]: number;
}

// number | string
// 这里允许 number 是因为索引签名特点,当索引签名类型为 string 时,也允许使用 number
type KeysNormal = keyof normal;

对类使用

1
2
3
4
5
6
7
8
9
10
11
12
13
class Normal {
value: string;
constructor(value: string) {
this.value = value;
}

nFun() {
return "normal";
}
}

// "value" | "nFun"
type KeysNormal = keyof Normal;

typeof

提取一个变量的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 解析函数
function normal(arg: string): boolean {
return !!arg;
}
// type Normal = (arg: string) => boolean
type Normal = typeof normal;

// 解析对象
const obj = {
label: "baka",
value: 9,
};
// type Obj = { label: string; value: number; }
type Obj = typeof obj;

提取 any 和 unknown 类型

anyunknown 进行类型提取时则有比较特殊的结果

1
2
3
4
// type Any = string | number | symbol
type Any = keyof any
// type Unknown = never
type Unknown = keyof unknown

infer

infer 关键字用于声明类型变量,以存储在模式匹配过程中所捕获的类型

下面这个示例就捕获到了 number[] 中的 number 类型

1
2
3
type Normal<T> = T extends (infer U)[] ? U : T;
// type T0 = number
type T0 = Normal<number[]>;

声明两个类型变量,捕获对象中的类型

1
2
3
4
5
6
7
8
type Normal<T> = T extends { label: infer U; value: infer R } ? [U, R] : T;

type Base = {
label: string;
value: number;
};
// type T0 = [string, number]
type T0 = Normal<Base>;

infer 使用注意:

  • 仅可以在条件类型extends 子句中使用
  • infer 声明的类型变量 仅可以在条件类型的 true 分支使用

协变与逆变

改编上面示例,两个 infer 均指向同一个变量,结果为联合类型 (协变位置)

1
2
3
4
5
6
7
8
type Normal<T> = T extends { label: infer U; value: infer U } ? U : T;

type Base = {
label: string;
value: number;
};
// type T0 = string | number
type T0 = Normal<Base>;

再做改编,将 infer 指向函数形参,结果为交叉类型 (逆变位置)

1
2
3
4
5
6
7
8
9
10
11
12
13
type Base = {
label: (arg: string) => void;
value: (arg: number) => void;
};

type Normal<T> = T extends {
label: (arg: infer U) => void;
value: (arg: infer U) => void;
}
? U
: T;
// type T0 = never (这里其实是 string & number = never, 变成了交叉类型)
type T0 = Normal<Base>;

函数参数逆变的,而对象属性协变的。
逆变位置的同一类型变量中的多个候选会被推断成交叉类型

基本结构

1
2
3
4
5
6
7
8
class Normal {
label: string;
value: number;
constructor() {
this.label = "";
this.value = 0;
}
}

构造函数

构造函数在执行 new 创建对象时执行,内部允许初始化类的成员属性

1
2
3
4
5
6
7
8
9
class Normal {
label: string;
constructor(label: string) {
this.label = label;
}
}

// "baka"
console.log(new Normal("baka").label);

有两点要注意:

  • 构造函数不能有类型参数
  • 构造函数不能有返回类型注释

允许对构造函数使用参数重载

1
2
3
4
5
6
7
8
class Normal {
label: string;
constructor(name: string, age: number);
constructor(sex: boolean);
constructor(ns: string | boolean, age?: number) {
// ...
}
}

参数属性

即在构造函数 constructor 参数上定义类成员,由 readonly、public、protected、private 等修饰

1
2
3
4
5
6
7
8
9
10
class Normal {
constructor(public label: string) {}

echo() {
console.log(this.label);
}
}

// baka
new Normal("baka").echo();

Getters/Setters

为两组方法,格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Normal {
label: string = "";
get title() {
return this.label;
}
// (parameter) title: string
set title(title) {
this.label = title;
}
}

const normal = new Normal();
// 自动执行 get 内的逻辑
const title = normal.title;
// 自动执行 set 内的逻辑
normal.title = "baka";

存在以下几点特性:

  • 如果存在 getter 但不存在 setter,该属性自动变为只读属性
  • 如果没有指定 setter 参数的类型,会自动从 getter 的返回值类型推断出来
  • getter 和 setter 必须设置相同的成员可见性

setter 的参数类型

Typescript 4.3 后,允许 setter 的参数类型为 getter 返回值的子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Normal {
label: string = "";
get title() {
return this.label;
}
set title(value: string | number) {
if (typeof value === "string") {
this.label = value;
}
}
}

const normal = new Normal();
normal.title = 114514;

索引签名

一旦限制索引签名,类中的属性和方法都将受到约束

1
2
3
4
5
6
7
8
9
class Normal {
[fieldName: string]: boolean | ((...args: string[]) => string);

label: boolean = true;

handle(arg: string) {
return arg;
}
}

implements

实现一个接口或类型别名,当一个类指定 implements 后,必须实现其中的所有必须属性。格式如下:

1
2
3
4
5
6
7
interface Normal {
label: string;
}

class NormalClass implements Normal {
label = "baka";
}

一个类可以实现多个 implements

1
2
3
4
5
6
7
8
9
10
11
12
interface Normal {
label: string;
}

interface Hard {
value: number;
}

class NormalClass implements Normal, Hard {
label = "baka";
value = 9;
}

implement 仅对 class 的成员进行限制,并不会给改变类的类型

1
2
3
4
5
6
7
8
9
10
11
interface Normal {
label: string;
value?: number;
}

class NormalClass implements Normal {
label = "baka";
}

// Property 'value' does not exist on type 'NormalClass'.
new NormalClass().value;

extends

(派生类)继承另一个类(基类),获取其所有private 修饰的属性

当一个类继承另一个类时,若该类存在 constructor 构造函数,则必须指定 super 调用。

若派生类未显示写明构造函数,则默认使用 基类 的构造函数。

super: 调用父类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
label: string = "baka";
}

class Normal extends Base {
// 派生类的构造函数必须包含 "super" 调用。
constructor() {
super();
}
}

// "baka"
console.log(new Normal().label);

派生类为基类的子类型,允许被赋值

1
const base: Base = new Normal();

重写基类方法

派生类允许使用和基类同名的方法,来对其进行重写

但必须要与基类方法兼容,如参数以及返回值类型,类似函数重载(属性也是如此)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
echo(): void {
console.log("baka");
}
}

class Normal extends Base {
echo(message?: string): void {
if (!message) {
// 调用父类方法
super.echo();
} else {
console.log(message);
}
}
}

初始化顺序

当派生类使用 new 关键字创建对象时,内部按照以下顺序执行

  • 字段初始化 (基类)
  • 构造函数运行 (基类)
  • 字段初始化 (派生类)
  • 构造函数运行 (派生类)

es5 继承内置对象问题

typescript.json 中的 target 设置为 es5 时,使用 extends 继承内置对象时会出现一些问题

1
2
3
4
5
6
7
8
9
10
class BakaError extends Error {
constructor(msg: string) {
super(msg);
}
echo() {
return `代码出错了笨蛋!: ${this.message}`;
}
}

const bakaError = new BakaError("abaaba");

编译未报错,运行如下代码,发现运行报错

1
2
3
4
// TypeError: bakaError.echo is not a function
console.log(bakaError.echo());
// false
console.log(bakaError instanceof BakaError);

原因:由于 es5 并没有 class 这种玩意儿,需要手动设置一下原型对象

对 constructor 进行如下修改

1
2
3
4
5
6
7
class BakaError extends Error {
constructor(msg: string) {
super(msg);
// 手动设置原型对象
(<any>Object).setPrototypeOf(this, BakaError.prototype);
}
}

再次执行,正常运行

1
2
3
4
// 代码出错了笨蛋!: abaaba
console.log(bakaError.echo());
// true
console.log(bakaError instanceof BakaError);

类中的 this

由于 js 遗留问题,类方法内的 this 可能会存在乱指向的问题, 比如下面示例

1
2
3
4
5
6
7
8
9
10
11
12
class Normal {
label = "baka";
getLabel() {
return this.label;
}
}
const obj = {
label: "baka is you",
getLabel: new Normal().getLabel,
};
// baka is you
console.log(obj.getLabel());

改编为 箭头函数 即可解决这种情况

1
2
3
4
5
6
7
8
9
10
11
12
class Normal {
label = "baka";
getLabel = () => {
return this.label;
};
}
const obj = {
label: "baka is you",
getLabel: new Normal().getLabel,
};
// baka
console.log(obj.getLabel());

注:改编为箭头函数后,派生类无法再使用 super 调用该方法。 typescript 并不会报错,但在运行时将会抛出错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
getLabel = () => {
console.log("baka");
};
}

class Normal extends Base {
constructor() {
super();
// TypeError: (intermediate value).getLabel is not a function
super.getLabel();
}
}

new Normal();

修饰符

readonly

被修饰的属性不允许在构造函数以外的地方进行修改

1
2
3
4
5
6
7
8
9
10
class Normal {
readonly label: string;
constructor() {
this.label = "";
}
test() {
// 无法分配到 "label" ,因为它是只读属性。
this.label = "?";
}
}

public

表示类成员在任何地方都可以访问,是默认值,不指定时默认为 public

protected

protected 修饰的成员只对当前类子类可见。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
protected label: string = "baka";
}

class Normal extends Base {
getLabel() {
// 允许访问
console.log(this.label);
}
}

// 报错:属性“label”受保护,只能在类“Base”及其子类中访问。
new Normal().label;

派生类可以暴露基类中 protocted 所修饰的成员。

1
2
3
4
5
6
7
8
9
10
class Base {
protected label: string = "baka";
}

class Normal extends Base {
// 暴露基类属性 label
public label: string = "baka is you";
}
// 允许访问
new Normal().label;

private

private 修饰的成员只对当前类可见。

static

static 修饰的成员为静态成员,访问其不需要实例化,直接用类名就可以访问

1
2
3
4
5
class Base {
static label: string = "baka";
}
// 允许访问
Base.label;

静态成员允许被继承,且可以被派生类直接用类名访问

1
2
3
4
5
6
7
class Base {
static label: string = "baka";
}

class Normal extends Base {}
// 允许访问
Normal.label;

es2015 以上支持使用 专用标识符 # 修饰静态成员,如下例的 #label
修饰后的成员变为私有属性,仅允许类内部通过类名调用。

static 支持修饰一个代码块,称为 static 区块,区块内同样可以访问 # 静态成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Normal {
static #label = "baka";

get label() {
return Normal.#label;
}

// static 区块
static {
this.#label = "baka is you";
}
}
// baka is you
console.log(new Normal().label);
// 报错:属性 "#label" 在类 "Normal" 外部不可访问,因为它具有专用标识符。
Normal.#label;
// 报错:属性 “#label” 在类型 “Normal” 上不存在。
new Normal().#label;

在泛型类中,static 修饰的成员是不能使用 类型参数

1
2
3
4
class Normal<T> {
// 报错:静态成员不能引用类类型参数。
static label: T;
}

类表达式

类允许像方法一样用表达式声明一个匿名类并赋值,同样允许使用泛型

1
2
3
4
5
const Normal = class<T> {
constructor(public label: T) {}
};
// "baka"
console.log(new Normal<string>("baka").label);

this 类型

在类中可以使用 this 类型来指向当前 this 所指向的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
label: string = "baka";
echo(other: this) {
return this.label;
}
}

class Normal extends Base {
value: number = 9;
}

// 报错:类型“Base”的参数不能赋给类型“Normal”的参数。
new Normal().echo(new Base());

this is Type

类和接口的方法的返回值的位置使用,缩小调用该方法的目标的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Normal<T> {
public label?: T;

hasLabel(): this is { label: T } {
return !!this.label;
}
}

const normal = new Normal<string>();
normal.label = "baka";
if (normal.hasLabel()) {
// const normal: Normal<string> & { label: string }
normal.label;
}

抽象类与抽象成员

使用 abstract 修饰后即变为抽象类,抽象类不能被实例化,只能被作为基类继承。

使用 abstract 修饰的成员为抽象成员,abstract 关键字于 public readonly 等之后使用,仅允许出现在抽象类中,不可以实现,子类继承后必须实现该成员。

1
2
3
4
5
6
7
8
9
10
11
abstract class Base {
// 不允许实现
public abstract readonly label: string;
// 非抽象成员,可以实现
public value: number = 9;
}

class Normal extends Base {
// 必须要实现 Base 中的抽象成员 label
public label: string = "baka";
}

类之间的细节补充

当两个类的成员类型完全相同时,是可以互相替代的

1
2
3
4
5
6
7
8
9
10
11
class Normal {
label: string = "baka";
value: number = 9;
}

class Hard {
label: string = "baka is you";
value: number = 6;
}
// 不会报错
const normal: Normal = new Hard();

当两个类的成员存在兼容关系但未显式继承时,也是存在隐式继承的

1
2
3
4
5
6
7
8
9
10
class Normal {
label: string = "baka";
}

class Hard {
label: string = "baka is you";
value: number = 6;
}
// 不会报错
const normal: Normal = new Hard();

针对空类时,任何类型都可以是它的实例对象

1
2
3
4
5
6
class Empty {}
// 均不会报错
const a: Empty = "baka";
const b: Empty = 9;
const c: Empty = {};
const d: Empty = () => {};

泛型

定义泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 泛型对象
type Normal<T> = {
value: T;
};

interface Normal<T> {
value: T;
}

// 泛型函数
type Normal<T> = (arg: T) => T;

interface Normal<T> {
(arg: T): T;
}

// 对象中的泛型函数
interface NormalFun {
<T>(ary: T): T;
}

自动推断

泛型是具备自动推断的,下面的示例自动将 T 推断为了 number

1
2
3
4
5
6
7
function genericsFunc<T>(tList: T[]): T | undefined {
return tList[0];
}
// num: number
const num = genericsFunc([1, 2, 3]);
// 手动注释写法
// const num = genericsFunc<number>([1, 2, 3])

定义多个泛型别名

1
2
3
4
5
6
7
8
9
function genericsFunc<I, O>(arr: I[], func: (arg: I[]) => O): O[] {
const output = func(arr);
return [output];
}

// output: string[]
const output = genericsFunc([1, 3], (arg: number[]): string => {
return arg.toString();
});

指定类型参数

有时候通过自动推断并不能实现预期效果,这时候就要手动添加类型

1
2
3
4
function combine<T>(arr1: T[], arr2: T[]): T[] {
return arr1.concat(arr2);
}
const arr = combine<number | string>([1, 2], ["a", "b"]);

泛型类

1
2
3
4
5
6
7
8
class Normal<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}

new Normal<number>(2);

泛型约束

有时需要使泛型变量受到某些约束,例如必须存在 length 属性,就需要对泛型进行限制

1
2
3
4
5
6
7
8
function checkLonger<T extends { length: number }>(a: T, b: T): T {
// 若不限制,会报错:类型 T 不存在 length 属性
return a.length > b.length ? a : b;
}
// longer: "mari" | "asuka"
const longer = checkLonger( "mari", "asuka" );
// 类型“number”的参数不能赋给类型“{ length: number; }”的参数。
// const longer = checkLonger(114, 514);

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface INormalArg {
label: string;
value: number;
}

function normal<T, K extends keyof T>(arg: T, key: K): T[K] {
return arg[key];
}

const arg: INormalArg = {
label: "baka",
value: 9,
};

// 这里第二个参数只能使用 "label" & "value"
const value = normal(arg, "value");

对 class 类型进行泛型约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Normal {
nFun() {
return "normal";
}
}

class Special extends Normal {
sFun() {
return "special";
}
}

function handle<F extends Normal>(c: new () => F): F {
return new c();
}

const spe = handle(Special);

// normal special
console.log(spe.nFun(), spe.sFun());

编写通用函数准则

  • 可以的话,使用类型本身,而不是对其约束
  • 尽可能少的使用指定类型参数
  • 如果一个类型参数只出现在一个地方,则需要考虑是否真的需要它

其他类型

索引访问类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Normal = {
label: string;
value: number;
};

interface INormal {
label: string;
value: number;
}

// type label = string
type label = Normal["label"];
// type value = number
type value = INormal["value"];

结合 keyof 使用,获取接口或类型别名的所有类型

1
2
3
4
5
6
7
type Normal = {
label: string;
value: number;
};

// type NormalValue = string | number
type NormalValue = Normal[keyof Normal];

结合 typeof 使用,通过特殊类型索引 number, 获取数组子项类型

*原因:数组的默认属性类型即为 number

1
2
3
4
5
6
7
8
9
const normalArray = [
{
label: "baka",
value: 9,
},
];

// type Normal = { label: string; value: number;}
type Normal = typeof normalArray[number];

映射类型

1
type Mapped<K> = { [ P in K ]: T }

P in K 类似 js 中的 for in 语句,用于遍历 K 中的所有类型,T 为你希望该属性所表示的任意类型

添加修饰符

允许使用修饰符,在修饰符前面添加 +- 表示 添加/移除 修饰符,默认为 +

使用 readonly? 修饰符:

1
2
3
4
// type Normal = { readonly x?: number | undefined; readonly y?: number | undefined }
type Normal = { readonly [ K in "x" | "y" ]?: number }
// 将接口的所有属性转换为 只读+必填
type ReadonlyAndRequired<T> = { readonly [ K in keyof T ]-?: T[K] }

as 重新映射

ts 4.1 开始支持,允许对键值重新映射,格式如下

1
type KeyRemapping<T> = [ K in keyof T as NewKeyType ]: T[K]

NewKeyType 必须为 string | number | symbol 的子类型

在对键进行重新映射时,若 as 子句返回 never 类型,该键将被删除

示例:将普通接口类型映射为 getter 格式的类型,即 getXxx: () => Type

1
2
3
4
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };

// type Normal = { getValue: () => number };
type Normal = Getters<{ value: number }>;

其中:

  • `` 为模板字面量类型,后面将会说明
  • Capitalize:将 string 类型转换为首字母大写
  • string & K:利用交叉类型返回 never,过滤非 string 类型

条件类型

类似 js 中的 三元表达式

格式

1
type Type = T extends U ? X : Y;

结合泛型使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Normal = {
label: string;
};

type Hard = {
value: number;
};

type Level<T extends number | string> = T extends number ? Normal : Hard;

function getLevel<T extends number | string>(params: T): Level<T> {
throw "";
}

// const level1: Normal
const level1 = getLevel(9);
// const level2: Hard
const level2 = getLevel("");
// const level3: Normal | Hard
const level3 = getLevel(Math.random() > 0.5 ? 9 : "");

结合 infer 关键字实现类型分发

下面这个案例封装了一个获取函数类型返回值的类型

1
2
3
4
5
6
7
8
type GetReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;

// type Normal = string
type Normal = GetReturnType<() => string>;

其实这就是预定义类型 ReturnType 的实现方法

分布式条件类型

如果被检查的类型,即 T extends U ? X : Y 中的 T 是一个裸类型参数,则该条件类型为分布式条件类型

裸类型参数:没有被数组(T[])、元组([T])或Promise(Promise<T>)包装过

对于分布式条件类型,若传入的被检查的参数为联合类型时,在运算过程中将被分解为多个分支

1
2
3
type Normal = A | B | C extends U ? X : Y
// 等同于
type Normal = (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

下面展示了是否是分布式条件类型时,传入的参数得到的最终结果的不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 分布式条件类型
type Normal<T> = T extends boolean ? "Y" : "N"
// type t0 = "N" | "Y"
type t0 = Normal<boolean | number>

// 非分布式条件类型
type WarppedTuple<T> = [T] extends [boolean] ? "Y" : "N"
type WarppedArrary<T> = T[] extends boolean[] ? "Y" : "N"
type WarppedPromise<T> = Promise<T> extends Promise<boolean> ? "Y" : "N"
// type T1 = "N"
type T1 = WarppedTuple<boolean | number>
// type T2 = "N"
type T2 = WarppedArrary<boolean | number>
// type T3 = "N"
type T3 = WarppedPromise<boolean | number>

模板字面量类型

ts 4.1 开始支持,写法类似 js 中的模板字符串,格式如下

其中类型占位符 T 允许为 string | number | boolean | bigint 的子类型,也可以传入对应类型的类型别名。

1
type Model<T> = `${T}`

使用示例:

1
2
3
4
type Base<T extends string> = `chiruno is ${ T }`

// type Normal = "chiruno is baka"
type Normal = Base<"baka">

当类型占位符 T 为联合类型时,联合类型会被自动展开
而对于包含多个类型占位符的情况时,多个联合类型将被展开并解析为叉积

1
2
3
4
5
6
7
8
9
// 联合类型自动展开
type Direction = "left" | "top" | "right" | "bottom";
// type CssPadding = "padding-left" | "padding-top" | "padding-right" | "padding-bottom"
type CssPadding = `padding-${Direction}`

// 多个联合类型作叉积
type Concat<T extends string, K extends string> = `${T}-${K}`
// type Hard = "top-true" | "top-false" | "bottom-true" | "bottom-false"
type Hard = Concat<"top" | "bottom", "true" | "false">

模板字面量类型可以将非字符串类型的基本类型字面量转换为对应的字符串类型字面量

1
2
3
4
type ToString<T extends number | string | boolean | bigint> = `${T}`

// type Normal = "baka" | "true" | "9" | "-1234"
type Normal = ToString<9 | "baka" | true | -1234n>

结合 infer条件类型 进行推断

1
2
3
4
5
type Direction = "left" | "top" | "right" | "bottom";
type Prefix<T> = T extends `${ infer R }-${ Direction }` ? R : T;

// type Normal = "margin"
type Normal = Prefix<"margin-left">

预定义类型

ReturnType

使用方式:ReturnType<T>,T 为一个函数类型

获得一个函数类型的返回值类型

1
2
3
4
type Normal = (arg: string) => boolean;

// type Ret = boolean
type Ret = ReturnType<Normal>;

Uppercase & Lowercase & Capitalize & Uncapitalize

分别为 全部大写全部小写首字母大写首字母小写, 仅对 string 类型进行处理的内置类型

1
2
// type Normal = "BAKA"
type Normal = Uppercase<"baka">

声明文件

npm 包的声明文件

与 npm 包绑定的声明文件

在两种情况下,声明文件会和 npm 包绑定

  • 在包目录下存在类型文件 index.d.ts
  • package.json 中显式的定义了 types 属性,指向具体位置的声明文件,如 "types": "index.d.ts"

第三方声明文件

有些时候,npm 包维护者并没有提供声明文件,而由社区中其他人向 @types 贡献了声明文件,可以尝试安装一下对应的 @types

1
npm i @types/[package name] --save-dev

默认情况下,typescript 会自动包含支持全局使用的任何声明定义,如安装 @types/node 后可以在全局使用 process
但如果你不希望被各种全局变量污染作用域,可以在 tsconfig.json 中作如下配置。

1
2
3
4
5
{
"compilerOptions": {
"types": ["node"]
}
}

像这样配置了 types 以后,将只允许使用 node 的 @types 包,即使安装了其他的 @types 包,也不会生效,除非同样将其加入到这里。

自行定义声明文件

若该 npm 包连第三方声明文件都不存在,这时候就只能自行定义声明文件了

  • 创建一个 types 目录,将 marry 的声明文件放到其中,目录为 types/marry/index.d.ts。并配置 tsconfig.json 中的 baseUrlpath 字段,如下。
1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"module": "commonjs",
"baseUrl": "./",
"paths": {
"*": ["types/*"]
}
}
}