1.类型守卫
{
const convertToUpperCase = (strOrArray: string | string[]) => {
if (typeof strOrArray === 'string') {
return strOrArray.toUpperCase();
} else if (Array.isArray(strOrArray)) {
return strOrArray.map(item => item.toUpperCase());
}
}
}
类型守卫的作用在于触发类型缩小。实际上,它还可以用来区分类型集合中的不同成员。
如何区分联合类型
常用的类型守卫包括switch、字面量恒等、typeof、instanceof、in 和自定义类型守卫
switch
{
const convert = (c: 'a' | 1) => {
switch (c) {
case 1:
return c.toFixed(); // c is 1
case 'a':
return c.toLowerCase(); // c is 'a'
}
}
const feat = (c: { animal: 'panda'; name: 'China' } | { feat: 'video'; name: 'Japan' }) => {
switch (c.name) {
case 'China':
return c.animal; // c is "{ animal: 'panda'; name: 'China' }"
case 'Japan':
return c.feat; // c is "{ feat: 'video'; name: 'Japan' }"
}
};
}
字面量恒等
const convert = (c: 'a' | 1) => {
if (c === 1) {
return c.toFixed(); // c is 1
} else if (c === 'a') {
return c.toLowerCase(); // c is 'a'
}
}
typeof
const convert = (c: 'a' | 1) => {
if (typeof c === 'number') {
return c.toFixed(); // c is 1
} else if (typeof c === 'string') {
return c.toLowerCase(); // c is 'a'
}
}
instanceof
{
class Dog {
wang = 'wangwang';
}
class Cat {
miao = 'miaomiao';
}
const getName = (animal: Dog | Cat) => {
if (animal instanceof Dog) {
return animal.wang;
} else if (animal instanceof Cat) {
return animal.miao;
}
}
}
in
const getName = (animal: Dog | Cat) => {
if ('wang' in animal) { // ok
return animal.wang; // ok
} else if ('miao' in animal) { // ok
return animal.miao; // ok
}
}
自定义类型守卫
const isDog = function (animal: Dog | Cat): animal is Dog {
return 'wang' in animal;
}
const getName = (animal: Dog | Cat) => {
if (isDog(animal)) {
return animal.wang;
}
}
如何区别枚举类型?
{
enum A {
one,
two
}
enum B {
one,
two
}
const cpWithNumber = (param: A) => {
if (param === 1) { // bad
return param;
}
}
const cpWithOtherEnum = (param: A) => {
if (param === B.two as unknown as A) { // ALERT bad
return param;
}
}
const cpWithSelf = (param: A) => {
if (param === A.two) { // good
return param;
}
}
}
失效的类型守卫
const getName = <T extends Dog | Cat>(animal: T) => {
if ('wang' in animal) {
return animal.wang; // ts(2339)
}
return animal.miao; // ts(2339)
};
//
const getName = <T extends Dog | Cat>(animal: T) => {
if (isDog(animal)) { // instanceOf 亦可
return animal.wang; // ok
}
return (animal as Cat).miao; // ts(2339)
};
2.类型兼容
特例
any
any 类型可以赋值给除了 never 之外的任意其他类型,反过来其他类型也可以赋值给 any。也就是说 any 可以兼容除 never 之外所有的类型,同时也可以被所有的类型兼容
never
never 的特性是可以赋值给任何其他类型,但反过来不能被其他任何类型(包括 any 在内)赋值
unknown
unknown 的特性和 never 的特性几乎反过来,即我们不能把 unknown 赋值给除了 any 之外任何其他类型,反过来其他类型都可以赋值给 unknown
void、null、undefined
void、null、undefined 这三大废材类型的兼容性也很特别,比如 void 类型仅可以赋值给 any 和 unknown 类型,反过来仅 any、never、undefined 可以赋值给 void
enum
数字枚举和数字类型相互兼容。 不同枚举之间不兼容
类型兼容性
子类型
从子类型的角度来看,所有的子类型与它的父类型都兼容
{
const one = 1;
let num: number = one; // ok
interface IPar {
name: string;
}
interface IChild extends IPar {
id: number;
}
let Par: IPar;
let Child: IChild;
Par = Child; // ok
class CPar {
cname = '';
}
class CChild extends CPar {
cid = 1;
}
let ParInst: CPar;
let ChildInst: CChild;
ParInst = ChildInst; // ok
let mixedNum: 1 | 2 | 3 = one; // ok
}
let ICPar: IPar | CPar;
let ICChild: IChild | CChild;
ICPar = ICChild; // ok
结构类型
类型兼容性的另一准则是结构类型,即如果两个类型的结构一致,则它们是互相兼容的。比如拥有相同类型的属性、方法的接口类型或类,则可以互相赋值。
{
class C1 {
name = '1';
}
class C2 {
name = '2';
}
interface I1 {
name: string;
}
interface I2 {
name: string;
}
let InstC1: C1;
let InstC2: C2;
let O1: I1;
let O2: I2;
InstC1 = InstC2; // ok
O1 = O2; // ok
InstC1 = O1; // ok
O2 = InstC2; // ok
}
两个接口类型或者类,如果其中一个类型不仅拥有另外一个类型全部的属性和方法,还包含其他的属性和方法(如同继承自另外一个类型的子类一样),那么前者是可以兼容后者的。
{
interface I1 {
name: string;
}
interface I2 {
id: number;
name: string;
}
class C2 {
id = 1;
name = '1';
}
let O1: I1;
let O2: I2;
let InstC2: C2;
O1 = O2;
O1 = InstC2;
}
虽然包含多余属性 id 的变量 O2 可以赋值给变量 O1,但是如果我们直接将一个与变量 O2 完全一样结构的对象字面量赋值给变量 O1,则会提示一个 ts(2322) 类型不兼容的错误(如下示例第 2 行),这就是对象字面的 freshness 特性。
O1 = {
id: 2, // ts(2322)
name: 'name'
};
let O3 = {
id: 2,
name: 'name'
};
O1 = O3; // ok
O1 = {
id: 2,
name: 'name'
} as I2; // ok
类兼容性特性:实际上,在判断两个类是否兼容时,我们可以完全忽略其构造函数及静态属性和方法是否兼容,只需要比较类实例的属性和方法是否兼容即可。如果两个类包含私有、受保护的属性和方法,则仅当这些属性和方法源自同一个类,它们才兼容。
{
class C1 {
name = '1';
private id = 1;
protected age = 30;
}
class C2 {
name = '2';
private id = 1;
protected age = 30;
}
let InstC1: C1;
let InstC2: C2;
InstC1 = InstC2; // ts(2322)
InstC2 = InstC1; // ts(2322)
}
{
class CPar {
private id = 1;
protected age = 30;
}
class C1 extends CPar {
constructor(inital: string) {
super();
}
name = '1';
static gender = 'man';
}
class C2 extends CPar {
constructor(inital: number) {
super();
}
name = '2';
static gender = 'woman';
}
let InstC1: C1;
let InstC2: C2;
InstC1 = InstC2; // ok
InstC2 = InstC1; // ok
}
可继承和可实现
类型兼容性还决定了接口类型和类是否可以通过 extends 继承另外一个接口类型或者类,以及类是否可以通过 implements 实现接口。
{
interface I1 {
name: number;
}
interface I2 extends I1 { // ts(2430)
name: string;
}
class C1 {
name = '1';
private id = 1;
}
class C2 extends C1 { // ts(2415)
name = '2';
private id = 1;
}
class C3 implements I1 {
name = ''; // ts(2416)
}
}
// 接口类型 I1 和接口类型 I2 包含不同类型的 name 属性不兼容,所以接口类型 I2 不能继承接口类型 I1。
// 类 C1 和类 C2 不满足类兼容条件,所以类 C2 也不能继承类 C1。
// 接口类型 I1 和类 C3 包含不同类型的 name 属性,所以类 C3 不能实现接口类型 I1。
泛型
泛型类型、泛型类的兼容性实际指的是将它们实例化为一个确切的类型后的兼容性。
变型
根据类型之间的子类型关系推断基于它们构造的更复杂类型之间的子类型关系。
使用数学中函数的表达方式。比如 Dog 类型,我们可以使用 F(Dog) 表示构造的复杂类型;F(Animal) 表示基于 Animal 构造的复杂类型。
- (1)协变
{
type isChild<Child, Par> = Child extends Par ? true : false;
interface Animal {
name: string;
}
interface Dog extends Animal {
woof: () => void;
}
type Covariance<T> = T;
type isCovariant = isChild<Covariance<Dog>, Covariance<Animal>>; // true
}
// Covariant<Dog> 是 Covariant<Animal> 的子类型,所以类型 isCovariant 是 true,这就是协变。
- (2)逆变
逆变也就是说如果 Dog 是 Animal 的子类型,则 F(Dog) 是 F(Animal) 的父类型,这与协变正好反过来。
际场景中,在我们推崇的 TypeScript 严格模式下,函数参数类型是逆变的,
type Contravariance<T> = (param: T) => void;
type isNotContravariance = isChild<Contravariance<Dog>, Contravariance<Animal>>; // false;
type isContravariance = isChild<Contravariance<Animal>, Contravariance<Dog>>; // true;
- (3)双向协变
双向协变也就是说如果 Dog 是 Animal 的子类型,则 F(Dog) 是 F(Animal) 的子类型,也是父类型,既是协变也是逆变。
对应到实际的场景,在 TypeScript 非严格模式下,函数参数类型就是双向协变的。如前边提到函数只有在参数是逆变的情况下才安全,且本课程一直在强调使用严格模式,所以双向协变并不是一个安全或者有用的特性,因此我们不大可能遇到这样的实际场景。
- (4)不变
不变即只要是不完全一样的类型,它们一定是不兼容的。也就是说即便 Dog 是 Animal 的子类型,如果 F(Dog) 不是 F(Animal) 的子类型,那么 F(Animal) 也不是 F(Dog) 的子类型。
interface Cat extends Animal {
miao: () => void;
}
const cat: Cat = {
name: 'Cat',
miao: () => void 0,
};
const dog: Dog = {
name: 'Dog',
woof: () => void 0,
};
let dogs: Dog[] = [dog];
animals = dogs; // ok
animals.push(cat); // ok
dogs.forEach(visitDog); // 类型 ok,但运行时会抛出错误
对于可变的数组而言,不变似乎是更安全、合理的设定。不过,在 TypeScript 中可变、不变的数组都是协变的,这是需要我们注意的一个陷阱。
函数参数类型是逆变的,返回值类型是协变的
函数类型兼容性
(1)返回值
前边我们已经讲过返回值类型是协变的,所以在参数类型兼容的情况下,函数的子类型关系与返回值子类型关系一致。也就是说返回值类型兼容,则函数兼容。
(2)参数类型
前边我们也讲过参数类型是逆变的,所以在参数个数相同、返回值类型兼容的情况下,函数子类型关系与参数子类型关系是反过来的(逆变)。
(3)参数个数
在索引位置相同的参数和返回值类型兼容的前提下,函数兼容性取决于参数个数,参数个数少的兼容个数多
{
let lessParams = (one: number) => void 0;
let moreParams = (one: number, two: string) => void 0;
lessParams = moreParams; // ts(2322)
moreParams = lessParams; // ok
}
(4)可选和剩余参数
可选参数可以兼容剩余参数、不可选参数,
let optionalParams = (one?: number, tow?: number) => void 0;
let requiredParams = (one: number, tow: number) => void 0;
let restParams = (...args: number[]) => void 0;
requiredParams = optionalParams; // ok
restParams = optionalParams; // ok
optionalParams = restParams; // ts(2322)
optionalParams = requiredParams; // ts(2322)
restParams = requiredParams; // ok
requiredParams = restParams; // ok
3.增强类型系统
如何在 TypeScript 中安全地使用 JavaScript 的库呢?关键的步骤就是使用 TypeScript 中的一个 declare 关键字。
通过使用 declare 关键字,我们可以声明全局的变量、方法、类、对象。
declare 变量
在运行时,前端代码
声明变量的语法: declare (var|let|const) 变量名称: 变量类型
declare var val1: string;
declare let val2: number;
declare const val3: boolean;
val1 = '1';
val1 = '2';
val2 = 1;
val2 = '2'; // TS2322: Type 'string' is not assignable to type 'number'.
val3 = true; // TS2588: Cannot assign to 'val3' because it is a constant.
声明函数
声明函数的语法与声明变量类型的语法相同,不同的是 declare 关键字后需要跟 function 关键字
declare function toString(x: number): string;
const x = toString(1); // => string
使用 declare关键字时,我们不需要编写声明的变量、函数、类的具体实现(因为变量、函数、类在其他库中已经实现了),只需要声明其类型即可
// TS1183: An implementation cannot be declared in ambient contexts.
declare function toString(x: number) {
return String(x);
};
// TypeScript 的报错信息提示:环境声明的上下文不需要实现。也就是说 declare 声明的所有类型只需要表明类型,不需要实现。
声明类
声明类时,我们只需要声明类的属性、方法的类型即可。
declare class Person {
public name: string;
private age: number;
constructor(name: string);
getAge(): number;
}
const person = new Person('Mike');
person.name; // => string
person.age; // TS2341: Property 'age' is private and only accessible within class 'Person'.
person.getAge(); // => number
声明枚举
声明枚举只需要定义枚举的类型,并不需要定义枚举的值
declare enum Direction {
Up,
Down,
Left,
Right,
}
const directions = [Direction.Up, Direction.Down, Direction.Left, Direction.Right];
注意:声明枚举仅用于编译时的检查,编译完成后,声明文件中的内容在编译结果中会被删除
declare 模块
声明模块的语法:declare module '模块名' {}。
// lodash.d.ts
declare module 'lodash' {
export function first<T extends unknown>(array: T[]): T;
}
// index.ts
import { first } from 'lodash';
first([1, 2, 3]); // => number;
声明文件是一个以.d.ts为后缀的文件。 在模块声明的内部,我们只需要使用 export 导出对应库的类、函数即可。
declare 文件
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare namespace
不同于声明模块,命名空间一般用来表示具有很多子属性或者方法的全局对象变量
declare namespace $ {
const version: number;
function ajax(settings?: any): void;
}
$.version; // => number
$.ajax();
//声明了全局导入的 jQuery 变量 $,所以可以直接使用 $ 变量的 version 属性以及 ajax 方法
声明文件
以 .d.ts 为后缀的文件为声明文件。 在声明文件时,我们只需要定义三方类库所暴露的 API 接口即可
类型
-
类型别名声明;
-
接口声明;
-
类声明;
-
枚举声明;
-
导入的类型声明。
上面的每一个声明都创建了一个类型名称。
值
值就是在运行时表达式可以赋予的值。
可以通过以下 6 种方式创建值:
-
var、let、const 声明;
-
namespace、module 包含值的声明;
-
枚举声明;
-
类声明;
-
导入的值;
-
函数声明。
命名空间
在命名空间中,我们也可以声明类型。比如 const x: A.B.C 这个声明,这里的类型 C 就是在 A.B 命名空间下的。
使用声明文件
安装 TypeScript 依赖后,一般我们会顺带安装一个 lib.d.ts 声明文件,这个文件包含了 JavaScript 运行时以及 DOM 中各种全局变量的声明
// typescript/lib/lib.d.ts
/// <reference no-default-lib="true"/>
/// <reference lib="es5" />
/// <reference lib="dom" />
/// <reference lib="webworker.importscripts" />
/// <reference lib="scripthost" />
使用 @types
Definitely Typed 是最流行性的高质量 TypeScript 声明文件类库
这里 搜索你想要导入的类库的类型声明
问题: 因为 Definitely Typed 是由社区人员维护的,如果原来的三方库升级,那么 Definitely Typed 所导出的三方库的类型定义想要升级还需要经过 PR、发布的流程,就会导致无法与原库保持完全同步。针对这个问题,在 TypeScript 中,我们可以通过类型合并、扩充类型定义的技巧临时解决。
类型合并
在 TypeScript 中,相同的接口、命名空间会依据一定的规则进行合并。
合并接口
最简单、常见的声明合并是接口合并
interface Person {
name: string;
}
interface Person {
age: number;
}
// 相当于
interface Person {
name: string;
age: number;
}
// 需要注意的是接口的非函数成员应该是类型兼容的
对于函数成员而言,每个同名的函数声明都会被当作这个函数的重载。后面声明的接口具有更高的优先级
interface Obj {
identity(val: any): any;
}
interface Obj {
identity(val: number): number;
}
interface Obj {
identity(val: boolean): boolean;
}
// 相当于
interface Obj {
identity(val: boolean): boolean;
identity(val: number): number;
identity(val: any): any;
}
const obj: Obj = {
identity(val: any) {
return val;
}
};
const t1 = obj.identity(1); // => number
const t2 = obj.identity(true); // => boolean
const t3 = obj.identity("t3"); // => any
合并 namespace
合并 namespace 与合并接口类似,命名空间的合并也会合并其导出成员的属性。不同的是,非导出成员仅在原命名空间内可见。
namespace Person {
const age = 18;
export function getAge() {
return age;
}
}
namespace Person {
export function getMyAge() {
return age; // TS2304: Cannot find name 'age'.
}
}
不可合并
对于类这个既是值又是类型的特殊对象不能合并。
扩充模块
// person.ts
export class Person {}
// index.ts
import { Person } from './person';
declare module './person' {
interface Person {
greet: () => void;
}
}
Person.prototype.greet = () => {
console.log('Hi!');
};
// 声明了导入模块 person 中 Person 的属性,TypeScript 会与原模块的类型合并,通过这种方式我们可以扩展导入模块的类型。同时,我们为导入的 Person 类增加了原型链上的 greet 方法。
// person.ts
export class Person {}
// index.ts
import { Person } from './person';
declare module './person' {
interface Person {
greet: () => void;
}
}
- declare module './person' {
- interface Person {
- greet: () => void;
- }
- }
+ // TS2339: Property 'greet' does not exist on type 'Person'.
Person.prototype.greet = () => {
console.log('Hi!');
};
// 删除了扩展模块的声明,第 20 行则会报出 ts(2339) 不存在 greet 属性的类型错误。
// 对于导入的三方模块,我们同样可以使用这个方法扩充原模块的属性。
扩充全局
全局模块指的是不需要通过 import 导入即可使用的模块,如全局的 window、document 等。
对全局对象的扩充与对模块的扩充是一样的
declare global {
interface Array<T extends unknown> {
getLen(): number;
}
}
Array.prototype.getLen = function () {
return this.length;
};
// 声明了全局的 Array 对象有一个 getLen 方法,所以为 Array 对象实现 getLen 方法时,TypeScript 不会报错。
4. 掌握官方工具类型
操作接口类型
Partial
Partial 工具类型可以将一个类型的所有属性变为可选的,且该工具类型返回的类型是给定类型的所有子集
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface Person {
name: string;
age?: number;
weight?: number;
}
type PartialPerson = Partial<Person>;
// 相当于
interface PartialPerson {
name?: string;
age?: number;
weight?: number;
}
Required
与 Partial 工具类型相反,Required 工具类型可以将给定类型的所有属性变为必填的
type Required<T> = {
[P in keyof T]-?: T[P];
};
type RequiredPerson = Required<Person>;
// 相当于
interface RequiredPerson {
name: string;
age: number;
weight: number;
}
映射类型在键值的后面使用了一个 - 符号,- 与 ? 组合起来表示去除类型的可选属性,因此给定类型的所有属性都变为了必填。
Readonly
Readonly 工具类型可以将给定类型的所有属性设为只读,这意味着给定类型的属性不可以被重新赋值
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type ReadonlyPerson = Readonly<Person>;
// 相当于
interface ReadonlyPerson {
readonly name: string;
readonly age?: number;
readonly weight?: number;
}
经过 Readonly 处理后,ReadonlyPerson 的 name、age、weight 等属性都变成了 readonly 只读。
Pick
Pick 工具类型可以从给定的类型中选取出指定的键值,然后组成一个新的类型
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type NewPerson = Pick<Person, 'name' | 'age'>;
// 相当于
interface NewPerson {
name: string;
age?: number;
}
Omit
与 Pick 类型相反,Omit 工具类型的功能是返回去除指定的键值之后返回的新类型
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type NewPerson = Omit<Person, 'weight'>;
// 相当于
interface NewPerson {
name: string;
age?: number;
}
操作接口类型的工具类型都使用了映射类型。通过映射类型,我们可以对原类型的属性进行重新映射,从而组成想要的类型。
联合类型
Exclude
在介绍 Omit 类型的实现中,我们使用了 Exclude 类型。通过使用 Exclude 类型,我们从接口的所有属性中去除了指定属性,因此,Exclude 的作用就是从联合类型中去除指定的类型。
type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<'a' | 'b' | 'c', 'a'>; // => 'b' | 'c'
type NewPerson = Omit<Person, 'weight'>;
// 相当于
type NewPerson = Pick<Person, Exclude<keyof Person, 'weight'>>;
// 其中
type ExcludeKeys = Exclude<keyof Person, 'weight'>; // => 'name' | 'age'
Exclude 的实现使用了条件类型。如果类型 T 可被分配给类型 U ,则不返回类型 T,否则返回此类型 T ,这样我们就从联合类型中去除了指定的类型。
Extract
Extract 类型的作用与 Exclude 正好相反,Extract 主要用来从联合类型中提取指定的类型,类似于操作接口类型中的 Pick 类型。
type Extract<T, U> = T extends U ? T : never;
type T = Extract<'a' | 'b' | 'c', 'a'>; // => 'a'
Extract 类型相当于取出两个联合类型的交集。
可以基于 Extract 实现一个获取接口类型交集的工具类型
type Intersect<T, U> = {
[K in Extract<keyof T, keyof U>]: T[K];
};
interface Person {
name: string;
age?: number;
weight?: number;
}
interface NewPerson {
name: string;
age?: number;
}
type T = Intersect<Person, NewPerson>;
// 相当于
type T = {
name: string;
age?: number;
};
NonNullable
NonNullable 的作用是从联合类型中去除 null 或者 undefined 的类型
type NonNullable<T> = T extends null | undefined ? never : T;
// 等同于使用 Exclude
type NonNullable<T> = Exclude<T, null | undefined>;
type T = NonNullable<string | number | undefined | null>; // => string | number
Record
Record 的作用是生成接口类型,然后我们使用传入的泛型参数分别作为接口类型的属性和值。
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type MenuKey = 'home' | 'about' | 'more';
interface Menu {
label: string;
hidden?: boolean;
}
const menus: Record<MenuKey, Menu> = {
about: { label: '关于' },
home: { label: '主页' },
more: { label: '更多', hidden: true },
};
这里的实现限定了第一个泛型参数继承自keyof any。
在 TypeScript 中,keyof any 指代可以作为对象键的属性
type T = keyof any; // => string | number | symbol
函数类型
ConstructorParameters
ConstructorParameters 可以用来获取构造函数的构造参数,而 ConstructorParameters 类型的实现则需要使用 infer 关键字推断构造参数的类型。
关于 infer 关键字,我们可以把它当成简单的模式匹配来看待。如果真实的参数类型和 infer 匹配的一致,那么就返回匹配到的这个类型。
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (
...args: infer P
) => any
? P
: never;
class Person {
constructor(name: string, age?: number) {}
}
type T = ConstructorParameters<typeof Person>; // [name: string, age?: number]
Parameters
Parameters 的作用与 ConstructorParameters 类似,Parameters 可以用来获取函数的参数并返回序对
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type T0 = Parameters<() => void>; // []
type T1 = Parameters<(x: number, y?: string) => void>; // [x: number, y?: string]
ReturnType
ReturnType 的作用是用来获取函数的返回类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type T0 = ReturnType<() => void>; // => void
type T1 = ReturnType<() => string>; // => string
ThisParameterType
ThisParameterType 可以用来获取函数的 this 参数类型。
type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;
type T = ThisParameterType<(this: Number, x: number) => void>; // Number
ThisType
ThisType 的作用是可以在对象字面量中指定 this 的类型。ThisType 不返回转换后的类型,而是通过 ThisType 的泛型参数指定 this 的类型
注意:如果你想使用这个工具类型,那么需要开启noImplicitThis的 TypeScript 配置。
type ObjectDescriptor<D, M> = {
data?: D;
methods?: M & ThisType<D & M>; // methods 中 this 的类型是 D & M
};
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {};
return { ...data, ...methods } as D & M;
}
const obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx; // this => D & M
this.y += dy; // this => D & M
},
},
});
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
OmitThisParameter
OmitThisParameter 工具类型主要用来去除函数类型中的 this 类型。如果传入的函数类型没有显式声明 this 类型,那么返回的仍是原来的函数类型。
type OmitThisParameter<T> = unknown extends ThisParameterType<T>
? T
: T extends (...args: infer A) => infer R
? (...args: A) => R
: T;
type T = OmitThisParameter<(this: Number, x: number) => string>; // (x: number) => string
字符串类型
模板字符串
TypeScript 自 4.1版本起开始支持模板字符串字面量类型。为此,TypeScript 也提供了 Uppercase、Lowercase、Capitalize、Uncapitalize这 4 种内置的操作字符串的类型
// 转换字符串字面量到大写字母
type Uppercase<S extends string> = intrinsic;
// 转换字符串字面量到小写字母
type Lowercase<S extends string> = intrinsic;
// 转换字符串字面量的第一个字母为大写字母
type Capitalize<S extends string> = intrinsic;
// 转换字符串字面量的第一个字母为小写字母
type Uncapitalize<S extends string> = intrinsic;
type T0 = Uppercase<'Hello'>; // => 'HELLO'
type T1 = Lowercase<T0>; // => 'hello'
type T2 = Capitalize<T1>; // => 'Hello'
type T3 = Uncapitalize<T2>; // => 'hello'
5. 类型编程: 打造属于自己的工具类型
本质就是自定义的复杂类型构造器(确切地讲是泛型)
泛型
type isSubTying<Child, Par> = Child extends Par ? true : false;
type isXX2 = isSubTyping<1, number>; // true
type isYY2 = isSubTyping<'string', string>; // true
type isZZ2 = isSubTyping<true, boolean>; // true
条件类型
三元运算的条件类型
TypeScript 支持使用三元运算的条件类型, 三元运算还支持嵌套
type isSubTyping<Child, Par> = Child extends Par ? true : false;
type isAssertable<T, S> = T extends S ? true : S extends T ? true : false;
type isNumAssertable = isAssertable<1, number>; // true
type isStrAssertable = isAssertable<string, 'string'>; // true
type isNotAssertable = isAssertable<1, boolean>; // false
分配条件类型
如果入参是联合类型,则会被拆解为一个个独立的(原子)类型(成员),然后再进行类型运算。
type BooleanOrString = string | boolean;
type StringOrNumberArray<E> = E extends string | number ? E[] : E;
type WhatIsThis = StringOrNumberArray<BooleanOrString>; // boolean | string[]
type BooleanOrStringGot = BooleanOrString extends string | number ? BooleanOrString[] : BooleanOrString; // string | boolean
// BooleanOrString 被当成了一个整体对待,所以 BooleanOrStringGot 的类型是 string | boolean。
通过某些手段强制类型入参被当成一个整体,也可以解除类型分配
type StringOrNumberArray<E> = [E] extends [string | number] ? E[] : E;
type WhatIsThis = StringOrNumberArray<string | boolean>; // string | boolean
// 使用 [] 将入参 E 包起来,即便入参是联合类型 string | boolean,也会被当成一个整体对待,所以返回的是 string | boolean。
注意:包含条件类型的泛型接收 never 作为泛型入参时,存在一定“陷阱”
type GetSNums = never extends number ? number[] : never extends string ? string[] : never; // number[];
type GetNever = StringOrNumberArray<never>; // never
never 是不能分配的底层类型,如果作为入参以原子形式出现在条件判断 extends 关键字左侧,则实例化得到的类型也是 never。
type UsefulNeverX<T> = T extends {} ? T[] : [];
type UselessNeverX<T, S> = S extends {} ? S[] : [];
type UselessNeverY<T, S> = S extends {} ? T[] : [];
type UselessNeverZ<T> = [T] extends [{}] ? T[] : [];
type ThisIsNeverX = UsefulNeverX<never>; // never
type ThisIsNotNeverX = UselessNeverX<never, string>; // string[]
type ThisIsNotNeverY = UselessNeverY<never, string>; // never[]
type ThisIsNotNeverZ = UselessNeverZ<never>; // never[]
条件类型中的类型推断 infer
在条件类型中使用类型推断操作符 infer 来获取类型入参的组成部分,比如说获取数组类型入参里元素的类型。
{
type ElementTypeOfArray<T> = T extends (infer E)[] ? E : never;
type isNumber = ElementTypeOfArray<number[]>; // number
type isNever = ElementTypeOfArray<number>; // never
}
还可以通过 infer 创建任意个类型推断参数,以此获取任意的成员类型
{
type ElementTypeOfObj<T> = T extends { name: infer E; id: infer I } ? [E, I] : never;
type isArray = ElementTypeOfObj<{ name: 'name'; id: 1; age: 30 }>; // ['name', 1]
type isNever = ElementTypeOfObj<number>; // never
}
索引访问类型
索引访问类型其实更像是获取物料的方式,首先我们可以通过属性名、索引、索引签名按需提取对象(接口类型)任意成员的类型(注意:只能使用 [索引名] 的语法)
interface MixedObject {
animal: {
type: 'animal' | 'dog' | 'cat';
age: number;
};
[name: number]: {
type: string;
age: number;
nickname: string;
};
[name: string]: {
type: string;
age: number;
};
}
type animal = MixedObject['animal'];
type animalType = MixedObject['animal']['type'];
type numberIndex = MixedObject[number];
type numberIndex0 = MixedObject[0];
type stringIndex = MixedObject[string];
type stringIndex0 = MixedObject['string'];
keyof
可以使用 keyof 关键字提取对象属性名、索引名、索引签名的类型
type MixedObjectKeys = keyof MixedObject; // string | number
type animalKeys = keyof animal; // 'type' | 'age'
type numberIndexKeys = keyof numberIndex; // "type" | "age" | "nickname"
typeof
如果我们在表达式上下文中使用 typeof,则是用来获取表达式值的类型,如果在类型上下文中使用,则是用来获取变量或者属性的类型。当然,在 TypeScript 中,typeof 的主要用途是在类型上下文中获取变量或者属性的类型
{
let StrA = 'a';
const unions = typeof StrA; // unions 类型是 "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
const str: typeof StrA = 'string'; // strs 类型是 string
type DerivedFromStrA = typeof StrA; // string
}
映射类型
可以使用索引签名语法和 in 关键字限定对象属性的范围
type SpecifiedKeys = 'id' | 'name';
type TargetType = {
[key in SpecifiedKeys]: any;
}; // { id: any; name: any; }
type TargetGeneric<O extends string | number | symbol> = {
[key in O]: any;
}
type TargetInstance = TargetGeneric<SpecifiedKeys>; // { id: any; name: any; }
注意:我们只能在类型别名定义中使用 in,如果在接口中使用,则会提示一个 ts(1169) 的错误
interface ITargetInterface {
[key in SpecifiedKeys]: any; // ts(1169)
}
在定义类型时,我们可以组合使用 in 和 keyof,并基于已有的类型创建一个新类型,使得新类型与已有类型保持一致的只读、可选特性,这样的泛型被称之为映射类型。
注意:in 和 keyof 也只能在类型别名定义中组合使用。
interface SourceInterface {
readonly id: number;
name?: string;
}
type TargetType = {
[key in keyof SourceInterface]: SourceInterface[key];
}; // { readonly id: number; name?: string | undefined }
type TargetGenericType<S> = {
[key in keyof S]: S[key];
};
type TargetInstance = TargetGenericType<SourceInterface>; // { readonly id: number; name?: string | undefined }
可以在映射类型中使用 readonly、? 修饰符来描述属性的可读性、可选性,也可以在修饰符前添加 +、- 前缀表示添加、移除指定修饰符(默认是 +、添加)
type TargetGenericTypeReadonly<S> = {
readonly [key in keyof S]: S[key];
}
type TargetGenericTypeReadonlyInstance = TargetGenericTypeReadonly<SourceInterface>; // { readonly id: number; readonly name?: string | undefined }
type TargetGenericTypeOptional<S> = {
[key in keyof S]?: S[key];
}
type TargetGenericTypeOptionalInstance = TargetGenericTypeOptional<SourceInterface>; // { readonly id?: number; readonly name?: string | undefined }
type TargetGenericTypeRemoveReadonly<S> = {
-readonly [key in keyof S]: S[key];
}
type TargetGenericTypeRemoveReadonlyInstance = TargetGenericTypeRemoveReadonly<SourceInterface>; // { id: number; name?: string | undefined }
type TargetGenericTypeRemoveOptional<S> = {
[key in keyof S]-?: S[key];
}
type TargetGenericTypeRemoveOptionalInstance = TargetGenericTypeRemoveOptional<SourceInterface>; // { readonly id: number; name: string }
使用 as 重新映射 key
自 TypeScript 4.1 起,我们可以在映射类型的索引签名中使用类型断言
type TargetGenericTypeAssertiony<S> = {
[key in keyof S as Exclude<key, 'id'>]: S[key];
}
type TargetGenericTypeAssertionyInstance = TargetGenericTypeAssertiony<SourceInterface>; // { name?: string | undefined; }
造轮子
Exclude
type ExcludeSpecifiedNumber = Exclude<1 | 2, 1>; // 2
type ExcludeSpecifiedString = Exclude<'id' | 'name', 'id'>; // 'name
type ExcludeSpecifiedBoolean = Exclude<boolean, true>; // false
代码实现如下:
type Exclude<T, U> = T extends U ? never : T;
ReturnTypeOfResolved
ReturnTypeOfResolved 和官方 ReturnType 的区别:如果入参 F 的返回类型是泛型 Promise 的实例,则返回 Promise 接收的入参。
// type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type ReturnTypeOfResolved<F extends (...args: any) => any> = F extends (...args: any[]) => Promise<infer R> ? R : ReturnType<F>;
type isNumber = ReturnTypeOfResolved<() => number>; // number
type isString = ReturnTypeOfResolved<() => Promise<string>>; // string
Merge
基于映射类型将类型入参 A 和 B 合并为一个类型的泛型 Merge<A, B>
type Merge<A, B> = {
[key in keyof A | keyof B]: key extends keyof A
? key extends keyof B
? A[key] | B[key]
: A[key]
: key extends keyof B
? B[key]
: never;
};
type Merged = Merge<{ id: number; name: string }, { id: string; age: number }>;
Equal
自定义工具类型 Equal<S, T>,它可以用来判断入参 S 和 T 是否是相同的类型。如果相同,则返回布尔字面量类型 true,否则返回 false。