TypeScript类型

15 阅读7分钟

常用类型

null

undefined

boolean

number

string

枚举

数组/元组

any/unknown

interface

可选属性

只读属性

接口合并

function

可选参数

默认参数

剩余参数

函数重载

泛型

泛型约束

类型别名

通过关键字 type 给类型起个别名,类型别名较多应⽤于联合类型、交叉类型这种复合类型。

type brand = string;
const str: brand = 'string';

type Tree<T, U> = {
 left: T;
 right: U;
};

interface VS type

类型别名看起来和接⼝⾮常类似,区别之处在于:

1、接⼝可以实现 extends 和 implements,类型别名不⾏。

2、在同⼀个命名空间下,同名的接⼝会合并,能添加新属性。

3、类型别名并不会创建新类型,是对原有类型的引⽤,⽽接⼝会定义⼀个新类型。

4、接⼝只能⽤于定义对象类型,⽽类型别名的声明⽅式除了对象之外还可以定义交叉、联合、原始类型等。

类型别名是最初 TypeScript 做类型约束的主要形式,后来引⼊接⼝之后,TypeScript 推荐我们尽可能的使⽤接⼝来规范我们的代码。

交叉类型

交叉类型 是将多个类型合并为⼀个类型。

语法

// 类型1 & 类型2

使用

interface Person {
 name: string; 
}

interface Clock {
 time: string; 
}

function foo(arg: Person & Clock) {
 console.log(arg.name);
 console.log(arg.time);
}

其中 arg 拥有 Person 和 Clock 的所有属性

联合类型

联合类型 表示取值为多种中的⼀种类型,⽽ 交叉类型 每次都是多个类型的合并类型。

语法

// 类型1 | 类型2

使用

interface Cat {
 name: string;
 fishing: () => void; 
}

interface Dog {
 name: string;
 barking: () => void; 
}

function foo(arg: Cat | Dog) {
 console.log(arg.name);
}

其中 arg 只能直接取到 name (Cat 和 Dog 公有的) 属性

实际上当 arg 的类型为 Cat 的时候我们应该能调⽤ fishing ⽅法, 所以我们判断⼀下类型

function foo(arg: Cat | Dog) {
 console.log(arg.name);
 // Property 'fishing' does not exist on type 'Cat | Dog'.
 // Property 'fishing' does not exist on type 'Dog'.ts
 if (arg.fishing) {
 arg.fishing;
 }
}

很遗憾我们不能直接这样判断, 我们需要使⽤ 类型断⾔ 处理 arg 的类型

类型断言

TypeScript 允许你覆盖它的推断,毕竟作为开发者你⽐编译器更了解你写的代码。

语法

<类型>值(值 as 类型);

当你在使⽤ JSX 语法时,会跟标签 <> 形式的类型断⾔混淆

所以,建议统⼀使⽤ as type 这种语法来为类型断⾔。

使用

function foo(arg: Cat | Dog) {
 console.log(arg.name);
 if ((arg as Cat).fishing) {
 (arg as Cat).fishing();
 }
}

这样就能调⽤到 Cat 的⽅法了

但是这样不是很优雅, 我们应该使⽤类型保护处理

类型保护

类型保护是指缩⼩类型的范围,在⼀定的块级作⽤域内由编译器推导其类型,提示并规避不合法的操作。

typeof

通过 typeof 运算符判断变量类型

function foo(arg: number | string) {
 if (typeof arg === 'string') {
 console.log(arg.length);
 } else {
 arg.toFixed();
 }
}

instanceof

instanceof 与 typeof 类似,区别在于 typeof 判断基础类型,instanceof 判断是否为某个对象

class Cat {
 fishing() {}
}
class Dog {
 barking() {}
}
function foo(arg: Cat | Dog) {
 if (arg instanceof Cat) {
 arg.fishing();
 } else {
 arg.barking();
 }
}

in

in 操作符⽤于确定属性是否存在于某个对象上,这也是⼀种缩⼩范围的类型保护。

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

interface Dog {
 name: string;
 barking: () => void; 
}

function foo(arg: Person | Dog) {
 if ('age' in arg) {
 console.log(arg.age);
 } else {
 arg.barking();
 }
}

使⽤ 'age' in arg 判断 arg 为 Person 类型

字⾯量类型保护

为每个类型增加⼀个相同的字段, 通过该字段区分类型

interface Person {
 type: 'person';
 name: string;
 age: number; 
}

interface Dog {
 type: 'dog';
 name: string;
 barking: () => void; 
}

function foo(arg: Person | Dog) {
 if (arg.type === 'person') {
 console.log(arg.age);
 } else {
 arg.barking();
 }
}

通过判断 arg.type 区分类型

用户自定义的类型保护

我们可能会将判断类型的逻辑提取为⼀个函数

// 类型判断函数
function isPerson(arg: Person | Dog) {
 return arg.type === 'person'; 
}

function foo(arg: Person | Dog) {
 if (isPerson(arg)) {
 // Property 'age' does not exist on type 'Person | Dog'.
 // Property 'age' does not exist on type 'Dog'.
 console.log(arg.age);
 } else {
 // Property 'barking' does not exist on type 'Person | Dog'.
 // Property 'barking' does not exist on type 'Person'.
    arg.barking();
 }
}

这样并没有让 ts 明⽩类型, 这样我们需要⽤到 is 关键字

is 关键字

is 关键字⼀般⽤于函数返回值类型中,判断参数是否属于某⼀类型,并根据结果返回对应的布尔类型

is 被称为类型谓词,⽤来判断⼀个变量属于某个接⼝或类型。

语法

// 变量 is 类型
const isString = (s: unknown): s is string => typeof val === 'string';

通过 is 关键字将类型范围缩⼩为 string 类型,这也是⼀种代码健壮性的约束规范

interface Person {
 type: 'person';
 name: string;
 age: number; 
}
interface Dog {
 type: 'dog';
 name: string;
 barking: () => void; 
}

function isPerson(arg: Person | Dog): arg is Person {
 return arg.type === 'person'; 
}

function foo(arg: Person | Dog) {
 if (isPerson(arg)) {
 console.log(arg.age);
 } else {
 arg.barking();
 }
}

is 关键字经常⽤来封装”类型判断函数”,通过和函数返回值的⽐较,从⽽缩⼩参数的类型范围,所以类型谓词 is 也是⼀种类型保护。

索引类型

keyof 索引类型查询操作符

type Foo = {
 a: number;
 b: string;
};
interface Person {
 name: string;
 age: number; 
}
type Bar = keyof Foo; // 'a' | 'b'
type P = keyof Person; // 'name' | 'age'

如果我们想要遍历⼀个变量的 key

const o = {
 a: 1,
};
type A = keyof o;
// 'o' refers to a value, but is being used as a type here. Did you mean 'typeof o'

T[K] 索引访问操作符

interface Person {
 name: string;
 age: number; 
}
type name = Person['name'];

⼀个对象的类型为泛型 T ,这个对象的属性类型 K 只需要满⾜ K extends keyof T ,即可得到这个属性值的类型为 T[K] 。

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
 return o[name]; // o[name] is of type T[K]
}

已知参数 o 的类型为 T,参数 name 的类型 K 满⾜ K extends keyof T ,那么返回值的类型即为 T[K] 。

映射类型

映射类型可以将已知类型的每个属性都变为可选的或者只读的。

Readonly 内置类型

将⼀个类型中的属性改为只读

使用

interface Person {
 name: string;
 age: number; 
}
type ReadonlyPerson = Readonly<Person>;

实现

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

这⾥就使⽤了映射类型的语法 [K in Keys] ,来看这个语法的两个部分:

  1. 类型变量 K :它会依次绑定到每个属性,对应每个属性名的类型。

  2. 字符串字⾯量构成的联合类型的 Keys :它包含了要迭代的属性名的集合。

Partial 和 Required 内置类型

将类型中所有属性改为可选或必选

使用

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

type PartialPerson = Partial<Person>;
type RequiredPartialPerson = Required<PartialPerson>;

实现

type Partial<T> = {
 [K in keyof T]?: T[K];
};

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

Pick 内置类型

选择⼀个类型中⼀部分的属性定义

使用

interface User {
 id: number;
 age: number;
 name: string; 
}
type PickUser = Pick<User, 'id'>;

实现

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

条件类型

条件类型 就是在初始状态并不直接确定具体类型,⽽是通过⼀定的类型运算得到最终的变量类型。

TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:

  • Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
  • Extract<T, U> -- 提取T中可以赋值给U的类型。
  • NonNullable -- 从T中剔除null和undefined。
  • ReturnType -- 获取函数返回值类型。
  • InstanceType -- 获取构造函数类型的实例类型。

语法

T extends U ? X : Y

语法类似三⽬运算符:若 T 是 U 的⼦类型,则类型为 X ,否则类型为 Y 。若⽆法确定 T 是否为 U 的⼦类型,则类型为 X | Y 。


获取类型字符串

type GetTypeStr<T> = T extends number
 ? 'number'
 : T extends string
 ? 'string'
 : 'type';

Exclude 内置类型

排除 联合类型 中的⼀部分内容

语法

type A = 'a' | 'b' | 'c';
// 排除 c
type ExcludeC = Exclude<A, 'c'>;

实现

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

这里T extends U,是指类型T可以分配给类型U

说明:

对于使用extends关键字的条件类型(即上面的三元表达式类型),如果extends前面的参数是一个泛型类型,当传入该参数的是联合类型,则使用 分配律 计算最终的结果。分配律是指, 将联合类型的联合项拆成单项 ,分别代入条件类型,然后将每个单项代入得到的结果再联合起来,得到最终的判断结果。

**

**

Extract 内置类型

取两个联合类型公有的部分

语法

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';
// 排除 a
type ExtractC = Extract<A, B>;

实现

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

Omit 内置类型

得到不包含某些字段的类型

语法

type Person = {
 name: string;
 age: number;
 addr: string;
};
type OmitPerson = Omit<Person, 'addr'>;

实现

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

infer 关键字

infer 的作⽤是让 TypeScript ⾃⼰推断,并将推断的结果存储到⼀个类型变量中, infer 只能⽤于 extends 语句中 (右边) 。

来看⼀下 ReturnType 的实现源码:

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

如果 T 满⾜约束条件 (...args: any) => any ,并且能够赋值给 (...args: any) => infer R ,则返回类型为 R ,否则为 any 类型。

模板类型文字

和 es ⾥的字符串模板很相似

type EventName<T extends string> = `${T}Changed`;
type T0 = EventName<'foo'>; // 'fooChanged'
type T1 = EventName<'foo' | 'bar' | 'baz'>; // 'fooChanged' | 'barChanged' |
'bazChanged'
type T3 = `${'top' | 'bottom'}-${'left' | 'right'}`;
// 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'

使用

ReturnType 的应用

const add = (x: number, y: number) => x + y;
type t = ReturnType<typeof add>; // type t = number

获取元组第一项类型

type A = ['a', 'b', 'c'];
type ArrayZero<T extends any[]> = T extends [infer S, ...infer R] ? S : never;
type B = ArrayZero<A>;

获取字符串第⼀个字符串

type A = 'abcd';
type StrZero<T extends string> = T extends `${infer Z}${infer R}`
 ? Z extends string
 ? Z
 : ''
 : '';
type B = StrZero<A>;