TypeScript -2
[toc]
模块
任何包含 import 或 export 语句的文件,就是一个模块(module)。相应地,如果文件不包含 export 语句,就是一个全局的脚本文件。
TypeScript 模块除了支持所有 ES 模块的语法,特别之处在于允许输出和输入类型。
export type Bool = true | false;
上面示例中,当前脚本输出一个类型别名Bool。
假定上面的模块文件为a.ts,另一个文件b.ts就可以使用 import 语句,输入这个类型。
import { Bool } from './a'; let foo:Bool = true;
import type 语句
import 在一条语句中,可以同时输入类型和正常接口。
// a.ts export interface A { foo: string; } export let a = 123; // b.ts import { A, a } from './a';
这样很不利于区分类型和正常接口,容易造成混淆。为了解决这个问题,TypeScript 引入了两个解决方法。
第一个方法是在 import 语句输入的类型前面加上type关键字。
import { type A, a } from './a';
上面示例中,import 语句输入的类型A前面有type关键字,表示这是一个类型。
第二个方法是使用 import type 语句,这个语句只能输入类型,不能输入正常接口。
// 正确 import type { A } from './a'; // 报错 import type { a } from './a';
同样的,export 语句也有两种方法,表示输出的是类型。
// 方法一 export {type A, type B}; // 方法二 export type {A, B};
namespace
官方已不推荐使用...
装饰器
==详情可见==:https://wangdoc.com/typescript/decorator
装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。
在语法上,装饰器有如下几个特征。
(1)第一个字符(或者说前缀)是@,后面是一个表达式。
(2)@后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
(3)这个函数接受所修饰对象的一些相关值作为参数。
(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
举例来说,有一个函数Injectable()当作装饰器使用,那么需要写成@Injectable,然后放在某个类的前面。
@Injectable class A { // ... }
上面示例中,由于有了装饰器@Injectable,类A的行为在运行时就会发生改变。
function simpleDecorator() { console.log('hi'); } @simpleDecorator class A {} // "hi"
上面示例中,函数simpleDecorator()用作装饰器,附加在类A之上,后者在代码解析时就会打印一行日志。
function simpleDecorator( value:any, context:any ) { console.log(`hi, this is ${context.kind} ${context.name}`); return value; } @simpleDecorator class A {} // "hi, this is class A"
类A在执行前会先执行装饰器simpleDecorator(),并且会向装饰器自动传入参数。
装饰器有多种形式,只要在@符号后面添加表达式都是可以的。注意,@后面的表达式,最终执行后得到的应该是一个函数。
相比使用子类改变父类,装饰器更加简洁优雅,缺点是不那么直观,功能也受到一些限制。所以,装饰器一般只用来为类添加某种特定行为。
装饰器的结构
装饰器函数的类型定义如下:
type Decorator = ( value: DecoratedValue, context: { kind: string; name: string | symbol; addInitializer?(initializer: () => void): void; static?: boolean; private?: boolean; access: { get?(): unknown; set?(value: unknown): void; }; } ) => void | ReplacementValue;
上面代码中,Decorator是装饰器的类型定义。它是一个函数,使用时会接收到value和context两个参数。
value:所装饰的对象。context:上下文对象,TypeScript 提供一个原生接口ClassMethodDecoratorContext,描述这个对象。
function decorator( value:any, context:ClassMethodDecoratorContext ) { // ... }
上面是一个装饰器函数,其中第二个参数context的类型就可以写成ClassMethodDecoratorContext。
context对象的属性,根据所装饰对象的不同而不同,其中只有两个属性(kind和name)是必有的,其他都是可选的。
(1)kind:字符串,表示所装饰对象的类型,可能取以下的值。
- 'class'
- 'method'
- 'getter'
- 'setter'
- 'field'
- 'accessor'
这表示一共有六种类型的装饰器。
(2)name:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。
(3)addInitializer():函数,用来添加类的初始化逻辑。以前,这些逻辑通常放在构造函数里面,对方法进行初始化,现在改成以函数形式传入addInitializer()方法。注意,addInitializer()没有返回值。
(4)private:布尔值,表示所装饰的对象是否为类的私有成员。
(5)static:布尔值,表示所装饰的对象是否为类的静态成员。
(6)access:一个对象,包含了某个值的 get 和 set 方法。
类装饰器
类装饰器的类型描述如下。
type ClassDecorator = ( value: Function, context: { kind: 'class'; name: string | undefined; addInitializer(initializer: () => void): void; } ) => Function | void;
类装饰器接受两个参数:value(当前类本身)和context(上下文对象)。其中,context对象的kind属性固定为字符串class。
类装饰器一般用来对类进行操作,可以不返回任何值:
function Greeter(value, context) { if (context.kind === 'class') { value.prototype.greet = function () { console.log('你好'); }; } } @Greeter class User {} let u = new User(); u.greet(); // "你好"
上面示例中,类装饰器@Greeter在类User的原型对象上,添加了一个greet()方法,实例就可以直接使用该方法。
类装饰器可以返回一个函数,替代当前类的构造方法。
function countInstances(value:any, context:any) { let instanceCount = 0; const wrapper = function (...args:any[]) { instanceCount++; const instance = new value(...args); instance.count = instanceCount; return instance; } as unknown as typeof MyClass; wrapper.prototype = value.prototype; // A return wrapper; } @countInstances class MyClass {} const inst1 = new MyClass(); inst1 instanceof MyClass // true inst1.count // 1
上面示例中,类装饰器@countInstances返回一个函数,替换了类MyClass的构造方法。新的构造方法实现了实例的计数,每新建一个实例,计数器就会加一,并且对实例添加count属性,表示当前实例的编号。
注意,上例为了确保新构造方法继承定义在MyClass的原型之上的成员,特别加入A行,确保两者的原型对象是一致的。否则,新的构造函数wrapper的原型对象,与MyClass不同,通不过instanceof运算符。
类装饰器也可以返回一个新的类,替代原来所装饰的类。
function countInstances(value:any, context:any) { let instanceCount = 0; return class extends value { constructor(...args:any[]) { super(...args); instanceCount++; this.count = instanceCount; } }; } @countInstances class MyClass {} const inst1 = new MyClass(); inst1 instanceof MyClass // true inst1.count // 1
上面示例中,@countInstances返回一个MyClass的子类。
类装饰器的上下文对象context的addInitializer()方法,用来定义一个类的初始化函数,在类完全定义结束后执行。
function customElement(name: string) { return <Input extends new (...args: any) => any>( value: Input, context: ClassDecoratorContext ) => { context.addInitializer(function () { customElements.define(name, value); }); }; } @customElement("hello-world") class MyComponent extends HTMLElement { constructor() { super(); } connectedCallback() { this.innerHTML = `<h1>Hello World</h1>`; } }
上面示例中,类MyComponent定义完成后,会自动执行类装饰器@customElement()给出的初始化函数,该函数会将当前类注册为指定名称(本例为<hello-world>)的自定义 HTML 元素。
方法装饰器
方法装饰器用来装饰类的方法(method)。它的类型描述如下。
type ClassMethodDecorator = ( value: Function, context: { kind: 'method'; name: string | symbol; static: boolean; private: boolean; access: { get: () => unknown }; addInitializer(initializer: () => void): void; } ) => Function | void;
参数value是方法本身,参数context是上下文对象,有以下属性。
kind:值固定为字符串method,表示当前为方法装饰器。name:所装饰的方法名,类型为字符串或 Symbol 值。static:布尔值,表示是否为静态方法。该属性为只读属性。private:布尔值,表示是否为私有方法。该属性为只读属性。access:对象,包含了方法的存取器,但是只有get()方法用来取值,没有set()方法进行赋值。addInitializer():为方法增加初始化函数。
方法装饰器会改写类的原始方法,实质等同于下面的操作。
function trace(decoratedMethod) { // ... } class C { @trace toString() { return 'C'; } } // `@trace` 等同于 // C.prototype.toString = trace(C.prototype.toString);
上面示例中,@trace是方法toString()的装饰器,它的效果等同于最后一行对toString()的改写。
如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数。
...
...
...
属性装饰器
属性装饰器用来装饰定义在类顶部的属性(field)。它的类型描述如下。
type ClassFieldDecorator = ( value: undefined, context: { kind: 'field'; name: string | symbol; static: boolean; private: boolean; access: { get: () => unknown, set: (value: unknown) => void }; addInitializer(initializer: () => void): void; } ) => (initialValue: unknown) => unknown | void;
注意,装饰器的第一个参数value的类型是undefined,这意味着这个参数实际上没用的,装饰器不能从value获取所装饰属性的值。另外,第二个参数context对象的kind属性的值为字符串field,而不是“property”或“attribute”,这一点是需要注意的。
属性装饰器要么不返回值,要么返回一个函数,该函数会自动执行,用来对所装饰属性进行初始化。该函数的参数是所装饰属性的初始值,该函数的返回值是该属性的最终值。
function logged(value, context) { const { kind, name } = context; if (kind === 'field') { return function (initialValue) { console.log(`initializing ${name} with value ${initialValue}`); return initialValue; }; } } class Color { @logged name = 'green'; } const color = new Color(); // "initializing name with value green"
上面示例中,属性装饰器@logged装饰属性name。@logged的返回值是一个函数,该函数用来对属性name进行初始化,它的参数initialValue就是属性name的初始值green。新建实例对象color时,该函数会自动执行。
...
...
getter 装饰器,setter 装饰器
...
...
accessor 装饰器
declare 关键字
declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。
它的主要作用,就是让当前文件可以使用其他文件声明的类型。举例来说,自己的脚本使用外部库定义的函数,编译器会因为不知道外部函数的类型定义而报错,这时就可以在自己的脚本里面使用declare关键字,告诉编译器外部函数的类型。这样的话,编译单个脚本就不会因为使用了外部类型而报错。
declare 关键字可以描述以下类型。
- 变量(const、let、var 命令声明)
- type 或者 interface 命令声明的类型
- class
- enum
- 函数(function)
- 模块(module)
- 命名空间(namespace)
declare 关键字的重要特点是,它只是通知编译器某个类型是存在的,不用给出具体实现。比如,只描述函数的类型,不给出函数的实现,如果不使用declare,这是做不到的。
declare 只能用来描述已经存在的变量和数据结构,不能用来声明新的变量和数据结构。另外,所有 declare 语句都不会出现在编译后的文件里面。
declare variable
declare 关键字可以给出外部变量的类型描述。
举例来说,当前脚本使用了其他脚本定义的全局变量x。
x = 123; // 报错
上面示例中,变量x是其他脚本定义的,当前脚本不知道它的类型,编译器就会报错。
这时使用 declare 命令给出它的类型,就不会报错了。
declare let x:number; x = 1;
如果 declare 关键字没有给出变量的具体类型,那么变量类型就是any。
declare let x; x = 1;
上面示例中,变量x的类型为any。
注意,declare 关键字只用来给出类型描述,是纯的类型代码,不允许设置变量的初始值,即不能涉及值。
declare function
declare 关键字可以给出外部函数的类型描述。
declare function sayHello( name:string ):void; sayHello('张三');
注意,这种单独的函数类型声明语句,只能用于declare命令后面。一方面,TypeScript 不支持单独的函数类型声明语句;另一方面,declare 关键字后面也不能带有函数的具体实现。
declare class
declare 给出 class 类型描述的写法如下:
declare class C { // 静态成员 public static s0():string; private static s1:string; // 属性 public a:number; private b:number; // 构造函数 constructor(arg:number); // 方法 m(x:number, y:number):number; // 存取器 get c():number; set c(value:number); // 索引签名 [index:string]:any; }
declare module
如果想把变量、函数、类组织在一起,可以将 declare 与 module 一起使用。
// 或者 declare module AnimalLib { class Animal { constructor(name:string); eat(): void; sleep(): void; } type Animals = 'Fish' | 'Dog'; }
declare module 里面,加不加 export 关键字都可以。
declare module 'io' { export function readFile(filename:string):string; }
某些第三方模块,原始作者没有提供接口类型,这时可以在自己的脚本顶部加上下面一行命令。
declare module "模块名"; // 例子 declare module "hot-new-module";
加上上面的命令以后,外部模块即使没有类型声明,也可以通过编译。但是,从该模块输入的所有接口都将为any类型。
declare global
如果要为 JavaScript 引擎的原生对象添加属性和方法,可以使用declare global {}语法。
export {}; declare global { interface String { toSmallString(): string; } } String.prototype.toSmallString = ():string => { // 具体实现 return ''; };
上面示例中,为 JavaScript 原生的String对象添加了toSmallString()方法。declare global 给出这个新增方法的类型描述。
这个示例第一行的空导出语句export {},作用是强制编译器将这个脚本当作模块处理。这是因为declare global必须用在模块里面。
declare module 用于类型声明文件
我们可以为每个模块脚本,定义一个.d.ts文件,把该脚本用到的类型定义都放在这个文件里面。但是,更方便的做法是为整个项目,定义一个大的.d.ts文件,在这个文件里面使用declare module定义每个模块脚本的类型。
d.ts类型声明文件
单独使用的模块,一般会同时提供一个单独的类型声明文件(declaration file),把本模块的外部接口的所有类型都写在这个文件里面,便于模块使用者了解接口,也便于编译器检查使用者的用法是否正确。
类型声明文件里面只有类型代码,没有具体的代码实现。它的文件名一般为[模块名].d.ts的形式,其中的d表示 declaration(声明)。
...
类型运算符
==new Bing:什么情况下才会用到以下运算符?==
keyof 运算符
keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。
// string | number | symbol type KeyT = keyof any;
由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol。
对于没有自定义键名的类型使用 keyof 运算符,返回never类型,表示不可能有这样类型的键名。
如果对象属性名采用索引形式,keyof 会返回属性名的索引类型。
// 示例一 interface T { [prop: number]: number; } // number type KeyT = keyof T; // 示例二 interface T { [prop: string]: string; } // string|number type KeyT = keyof T;
上面的示例二,keyof T返回的类型是string|number,原因是 JavaScript 属性名为字符串时,包含了属性名为数值的情况,因为数值属性名会自动转为字符串。
如果 keyof 运算符用于数组或元组类型,得到的结果可能出人意料。
type Result = keyof ['a', 'b', 'c']; // 返回 number | "0" | "1" | "2" // | "length" | "pop" | "push" | ···
上面示例中,keyof 会返回数组的所有键名,包括数字键名和继承的键名。
对于联合类型,keyof 返回成员共有的键名。
type A = { a: string; z: boolean }; type B = { b: string; z: boolean }; // 返回 'z' type KeyT = keyof (A | B);
对于交叉类型,keyof 返回所有键名。
type A = { a: string; x: boolean }; type B = { b: string; y: number }; // 返回 'a' | 'x' | 'b' | 'y' type KeyT = keyof (A & B); // 相当于 keyof (A & B) ≡ keyof A | keyof B
keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型,可以像下面这样写。
type MyObj = { foo: number, bar: string, }; type Keys = keyof MyObj; type Values = MyObj[Keys]; // number|string
上面示例中,Keys是键名组成的联合类型,而MyObj[Keys]会取出每个键名对应的键值类型,组成一个新的联合类型,即number|string。
keyof 运算符的用途
keyof 运算符往往用于精确表达对象的属性类型。
function prop( obj: { [p:string]: any }, key: string ):any { return obj[key]; }
上面的类型声明有两个问题,一是无法表示参数key与参数obj之间的关系,二是返回值类型只能写成any。
有了 keyof 以后,就可以解决这两个问题,精确表达返回值类型。
function prop<Obj, K extends keyof Obj>( obj:Obj, key:K ):Obj[K] { return obj[key]; }
keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。
type NewProps<Obj> = { [Prop in keyof Obj]: boolean; }; // 用法 type MyObj = { foo: number; }; // 等于 { foo: boolean; } type NewObj = NewProps<MyObj>;
上面示例中,类型NewProps是类型Obj的映射类型,前者继承了后者的所有属性,但是把所有属性值类型都改成了boolean。
下面的例子是去掉 readonly 修饰符。
type Mutable<Obj> = { -readonly [Prop in keyof Obj]: Obj[Prop]; }; // 用法 type MyObj = { readonly foo: number; } // 等于 { foo: number; } type NewObj = Mutable<MyObj>;
上面示例中,[Prop in keyof Obj]是Obj类型的所有属性名,-readonly表示去除这些属性的只读特性。对应地,还有+readonly的写法,表示添加只读属性设置。-?表示去除可选属性设置。对应地,还有+?的写法,表示添加可选属性设置。
in 运算符
JavaScript 语言中,in运算符用来确定对象是否包含某个属性名。
TypeScript 语言的类型运算中,in运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。
type U = 'a'|'b'|'c'; type Foo = { [Prop in U]: number; }; // 等同于 type Foo = { a: number, b: number, c: number };
上面示例中,[Prop in U]表示依次取出联合类型U的每一个成员。
方括号运算符
方括号运算符([])用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。
type Person = { age: number; name: string; alive: boolean; }; // Age 的类型是 number type Age = Person['age'];
上面示例中,Person['age']返回属性age的类型,本例是number。
方括号的参数如果是联合类型,那么返回的也是联合类型。
type Person = { age: number; name: string; alive: boolean; }; // number|string type T = Person['age'|'name']; // number|string|boolean type A = Person[keyof Person];
方括号运算符的参数也可以是属性名的索引类型。
type Obj = { [key:string]: number, }; // number type T = Obj[string];
上面示例中,Obj的属性名是字符串的索引类型,所以可以写成Obj[string],代表所有字符串属性名,返回的就是它们的类型number。
这个语法对于数组也适用,可以使用number作为方括号的参数。
// MyArray 的类型是 { [key:number]: string } const MyArray = ['a','b','c']; // 等同于 (typeof MyArray)[number] // 返回 string type Person = typeof MyArray[number];
上面示例中,MyArray是一个数组,它的类型实际上是属性名的数值索引,而typeof MyArray[number]的typeof运算优先级高于方括号,所以返回的是所有数值键名的键值类型string。
注意,方括号里面不能有值的运算。
extends...?: 条件运算符
TypeScript 提供类似 JavaScript 的?:运算符这样的三元运算符,但多出了一个extends关键字。
条件运算符extends...?:可以根据当前类型是否符合某种条件,返回不同的类型。
T extends U ? X : Y
上面式子中的extends用来判断,类型T是否可以赋值给类型U,即T是否为U的子类型,这里的T和U可以是任意类型。
如果T能够赋值给类型U,表达式的结果为类型X,否则结果为类型Y。
// true type T = 1 extends number ? true : false;
上面示例中,1是number的子类型,所以返回true。
如果需要判断的类型是一个联合类型,那么条件运算符会展开这个联合类型。
(A|B) extends U ? X : Y // 等同于 (A extends U ? X : Y) | (B extends U ? X : Y)
如果不希望联合类型被条件运算符展开,可以把extends两侧的操作数都放在方括号里面。
type ToArray<Type> = [Type] extends [any] ? Type[] : never; // (string | number)[] type T = ToArray<string|number>;
extends两侧的运算数都放在方括号里面,所以传入的联合类型不会展开,返回的是一个数组。
infer 关键字
infer关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。
它通常跟条件运算符一起使用,用在extends关键字后面的父类型之中。
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
上面示例中,infer Item表示Item这个参数是 TypeScript 自己推断出来的,不用显式传入,而Flatten<Type>则表示Type这个类型参数是外部传入的。Type extends Array<infer Item>则表示,如果参数Type是一个数组,那么就将该数组的成员类型推断为Item,即Item是从Type推断出来的。
一旦使用Infer Item定义了Item,后面的代码就可以直接调用Item了。下面是上例的泛型Flatten<Type>的用法。
// string type Str = Flatten<string[]>; // number type Num = Flatten<number>;
上面示例中,第一个例子Flatten<string[]>传入的类型参数是string[],可以推断出Item的类型是string,所以返回的是string。第二个例子Flatten<number>传入的类型参数是number,它不是数组,所以直接返回自身。
如果不用infer定义类型参数,那么就要传入两个类型参数。
type Flatten<Type, Item> = Type extends Array<Item> ? Item : Type;
上面是不使用infer的写法,每次调用Flatten的时候,都要传入两个参数,就比较麻烦。
下面的例子使用infer,推断函数的参数类型和返回值类型。
type ReturnPromise<T> = T extends (...args: infer A) => infer R ? (...args: A) => Promise<R> : T;
上面示例中,如果T是函数,就返回这个函数的 Promise 版本,否则原样返回。infer A表示该函数的参数类型为A,infer R表示该函数的返回值类型为R。
is 运算符
函数返回布尔值的时候,可以使用is运算符,限定返回值与参数之间的关系。
is运算符用来描述返回值属于true还是false。
function isFish( pet: Fish|Bird ):pet is Fish { return (pet as Fish).swim !== undefined; }
is运算符总是用于描述函数的返回值类型,写法采用parameterName is Type的形式,即左侧为当前函数的参数名,右侧为某一种类型。它返回一个布尔值,表示左侧参数是否属于右侧的类型。
type A = { a: string }; type B = { b: string }; function isTypeA(x: A|B): x is A { if ('a' in x) return true; return false; }
上面示例中,返回值类型x is A可以准确描述函数体内部的运算逻辑。
is运算符可以用于类型保护。
上面示例中,函数isCat()的返回类型是a is Cat,它是一个布尔值。后面的if语句就用这个返回值进行判断,从而起到类型保护的作用,确保x是 Cat 类型,从而x.meow()不会报错(假定Cat类型拥有meow()方法)。
is运算符还有一种特殊用法,就是用在类(class)的内部,描述类的方法的返回值。
class Teacher { isStudent():this is Student { return false; } } class Student { isStudent():this is Student { return true; } }
注意,this is T这种写法,只能用来描述方法的返回值类型,而不能用来描述属性的类型。
模板字符串
TypeScript 允许使用模板字符串,构建类型。
模板字符串的最大特点,就是内部可以引用其他类型。
type World = "world"; // "hello world" type Greeting = `hello ${World}`;
注意,模板字符串可以引用的类型一共6种,分别是 string、number、bigint、boolean、null、undefined。引用这6种以外的类型会报错。
模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串可以展开联合类型。
type T = 'A'|'B'; // "A_id"|"B_id" type U = `${T}_id`;
如果模板字符串引用两个联合类型,它会交叉展开这两个类型。
type T = 'A'|'B'; type U = '1'|'2'; // 'A1'|'A2'|'B1'|'B2' type V = `${T}${U}`;
satisfies 运算符
...
类型映射
映射(mapping)指的是,将一种类型按照映射规则,转换成另一种类型,通常用于对象类型。
举例来说,现有一个类型A和另一个类型B。
type A = { foo: number; bar: number; }; type B = { foo: string; bar: string; };
上面示例中,这两个类型的属性结构是一样的,但是属性的类型不一样。如果属性数量多的话,逐个写起来就很麻烦。
使用类型映射,就可以从类型A得到类型B。
type A = { foo: number; bar: number; }; type B = { [prop in keyof A]: string; };
上面示例中,类型B采用了属性名索引的写法,[prop in keyof A]表示依次得到类型A的所有属性名,然后将每个属性的类型改成string。
为了增加代码复用性,可以把常用的映射写成泛型。
type ToBoolean<Type> = { [Property in keyof Type]: boolean; };
TypeScript内置的工具类型Readonly<T>可以将所有属性改为只读属性,实现也是通过映射。
// 将 T 的所有属性改为只读属性 type Readonly<T> = { readonly [P in keyof T]: T[P]; };
映射修饰符
映射会原样复制原始对象的可选属性和只读属性。
如果要删改可选和只读这两个特性,并不是很方便。为了解决这个问题,TypeScript 引入了两个映射修饰符,用来在映射时添加或移除某个属性的?修饰符和readonly修饰符。
+修饰符:写成+?或+readonly,为映射属性添加?修饰符或readonly修饰符。–修饰符:写成-?或-readonly,为映射属性移除?修饰符或readonly修饰符。
下面是添加或移除可选属性的例子。
// 添加可选属性 type Optional<Type> = { [Prop in keyof Type]+?: Type[Prop]; }; // 移除可选属性 type Concrete<Type> = { [Prop in keyof Type]-?: Type[Prop]; };
注意,+?或-?要写在属性名的后面。
下面是添加或移除只读属性的例子。
// 添加 readonly type CreateImmutable<Type> = { +readonly [Prop in keyof Type]: Type[Prop]; }; // 移除 readonly type CreateMutable<Type> = { -readonly [Prop in keyof Type]: Type[Prop]; };
注意,+readonly和-readonly要写在属性名的前面。
TypeScript 原生的工具类型Required<T>专门移除可选属性,就是使用-?修饰符实现的。
另外,+?修饰符可以简写成?,+readonly修饰符可以简写成readonly。
键名重映射
TypeScript 4.1 引入了键名重映射(key remapping),允许改变键名。
type A = { foo: number; bar: number; }; type B = { [p in keyof A as `${p}ID`]: number; }; // 等同于 type B = { fooID: number; barID: number; };
下面是另一个例子:
interface Person { name: string; age: number; location: string; } type Getters<T> = { [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]; }; type LazyPerson = Getters<Person>; // 等同于 type LazyPerson = { getName: () => string; getAge: () => number; getLocation: () => string; }
它的修改键名的代码是一个模板字符串get${Capitalize<string & P>},下面是各个部分的解释。
-
get:为键名添加的前缀。 -
Capitalize<T>:一个原生的工具泛型,用来将T的首字母变成大写。 -
string & P:一个交叉类型,其中的P是 keyof 运算符返回的键名联合类型string|number|symbol,但是Capitalize<T>只能接受字符串作为类型参数,因此string & P只返回P的字符串属性名。
属性过滤
键名重映射还可以过滤掉某些属性。下面的例子是只保留字符串属性。
type User = { name: string, age: number } type Filter<T> = { [K in keyof T as T[K] extends string ? K : never]: string } type FilteredUser = Filter<User> // { name: string }
它的键名重映射as T[K] extends string ? K : never],使用了条件运算符。如果属性值T[K]的类型是字符串,那么属性名不变,否则属性名类型改为never,即这个属性名不存在。这样就等于过滤了不符合条件的属性,只保留属性值为字符串的属性。
联合类型的映射
由于键名重映射可以修改键名类型,所以原始键名的类型不必是string|number|symbol,任意的联合类型都可以用来进行键名重映射。
type S = { kind: 'square', x: number, y: number, }; type C = { kind: 'circle', radius: number, }; type MyEvents<Events extends { kind: string }> = { [E in Events as E['kind']]: (event: E) => void; } type Config = MyEvents<S|C>; // 等同于 type Config = { square: (event:S) => void; circle: (event:C) => void; }
上面示例中,原始键名的映射是E in Events,这里的Events是两个对象组成的联合类型S|C。所以,E是一个对象,然后再通过键名重映射,得到字符串键名E['kind']。
类型工具
TypeScript 内置了17个类型工具,可以直接使用。
1. Awaited<Type>
Awaited<Type>用来取出 Promise 的返回值类型,适合用在描述then()方法和 await 命令的参数类型。
// string type A = Awaited<Promise<string>>;
上面示例中,Awaited<Type>会返回 Promise 的返回值类型(string)。
它也可以返回多重 Promise 的返回值类型。
// number type B = Awaited<Promise<Promise<number>>>;
如果它的类型参数不是 Promise 类型,那么就会原样返回。
// number | boolean type C = Awaited<boolean | Promise<number>>;
上面示例中,类型参数是一个联合类型,其中的boolean会原样返回,所以最终返回的是number|boolean。
2. ConstructorParameters<Type>
ConstructorParameters<Type>提取构造方法Type的参数类型,组成一个元组类型返回。
type T1 = ConstructorParameters< new (x: string, y: number) => object >; // [x: string, y: number] type T2 = ConstructorParameters< new (x?: string) => object >; // [x?: string | undefined]
它可以返回一些内置构造方法的参数类型。如果参数类型不是构造方法,就会报错。
any类型和never类型是两个特殊值,分别返回unknown[]和never。
type T1 = ConstructorParameters<any>; // unknown[] type T2 = ConstructorParameters<never>; // never
3. Exclude<UnionType, ExcludedMembers>
Exclude<UnionType, ExcludedMembers>用来从联合类型UnionType里面,删除某些类型ExcludedMembers,组成一个新的类型返回。
type T1 = Exclude<'a'|'b'|'c', 'a'>; // 'b'|'c' type T2 = Exclude<'a'|'b'|'c', 'a'|'b'>; // 'c' type T3 = Exclude<string|(() => void), Function>; // string type T4 = Exclude<string | string[], any[]>; // string type T5 = Exclude<(() => void) | null, Function>; // null type T6 = Exclude<200 | 400, 200 | 201>; // 400 type T7 = Exclude<number, boolean>; // number
4. Extract<Type, Union>
Extract<UnionType, Union>用来从联合类型UnionType之中,提取指定类型Union,组成一个新类型返回。它与Exclude<T, U>正好相反。
type T1 = Extract<'a'|'b'|'c', 'a'>; // 'a' type T2 = Extract<'a'|'b'|'c', 'a'|'b'>; // 'a'|'b' type T3 = Extract<'a'|'b'|'c', 'a'|'d'>; // 'a' type T4 = Extract<string | string[], any[]>; // string[] type T5 = Extract<(() => void) | null, Function>; // () => void type T6 = Extract<200 | 400, 200 | 201>; // 200
如果参数类型Union不包含在联合类型UnionType之中,则返回never类型。
type T = Extract<string|number, boolean>; // never
5. InstanceType<Type>
InstanceType<Type>提取构造函数的返回值的类型(即实例类型),参数Type是一个构造函数,等同于构造函数的ReturnType<Type>。
type T = InstanceType< new () => object >; // object
上面示例中,类型参数是一个构造函数new () => object,返回值是该构造函数的实例类型(object)。
下面是一些例子:
type A = InstanceType<ErrorConstructor>; // Error type B = InstanceType<FunctionConstructor>; // Function type C = InstanceType<RegExpConstructor>; // RegExp
由于 Class 作为类型,代表实例类型。要获取它的构造方法,必须把它当成值,然后用typeof运算符获取它的构造方法类型。
class C { x = 0; y = 0; } type T = InstanceType<typeof C>; // C
上面示例中,typeof C是C的构造方法类型,然后 InstanceType 就能获得实例类型,即C本身。
如果类型参数不是构造方法,就会报错。如果类型参数是any或never两个特殊值,分别返回any和never。
6. NonNullable<Type>
NonNullable<Type>用来从联合类型Type删除null类型和undefined类型,组成一个新类型返回,也就是返回Type的非空类型版本。
// string|number type T1 = NonNullable<string|number|undefined>;
7. Omit<Type, Keys>
Omit<Type, Keys>用来从对象类型Type中,删除指定的属性Keys,组成一个新的对象类型返回。
interface A { x: number; y: number; } type T1 = Omit<A, 'x'>; // { y: number } type T2 = Omit<A, 'y'>; // { x: number } type T3 = Omit<A, 'x' | 'y'>; // { }
指定删除的键名Keys可以是对象类型Type中不存在的属性,但必须兼容string|number|symbol。
9. OmitThisParameter<Type>
OmitThisParameter<Type>从函数类型中移除 this 参数。
function toHex(this: Number) { return this.toString(16); } type T = OmitThisParameter<typeof toHex>; // () => string
如果函数没有 this 参数,则返回原始函数类型。
10. Parameters<Type>
Parameters<Type>从函数类型Type里面提取参数类型,组成一个元组返回。
type T1 = Parameters<() => string>; // [] type T2 = Parameters<(s:string) => void>; // [s:string]
如果参数类型Type不是带有参数的函数形式,会报错。
由于any和never是两个特殊值,会返回unknown[]和never。
11. Partial<Type>
Partial<Type>返回一个新类型,将参数类型Type的所有属性变为可选属性。
interface A { x: number; y: number; } type T = Partial<A>; // { x?: number; y?: number; }
12. Pick<Type, Keys>
Pick<Type, Keys>返回一个新的对象类型,第一个参数Type是一个对象类型,第二个参数Keys是Type里面被选定的键名。
interface A { x: number; y: number; } type T1 = Pick<A, 'x'>; // { x: number } type T2 = Pick<A, 'y'>; // { y: number } type T3 = Pick<A, 'x'|'y'>; // { x: number; y: number }
上面示例中,Pick<Type, Keys>会从对象类型A里面挑出指定的键名,组成一个新的对象类型。
指定的键名Keys必须是对象键名Type里面已经存在的键名,否则会报错。
13. Readonly<Type>
Readonly<Type>返回一个新类型,将参数类型Type的所有属性变为只读属性。
interface A { x: number; y?: number; } // { readonly x: number; readonly y?: number; } type T = Readonly<A>;
14. Record<Keys, Type>
Record<Keys, Type>返回一个对象类型,参数Keys用作键名,参数Type用作键值类型。
// { a: number } type T = Record<'a', number>;
上面示例中,Record<Keys, Type>的第一个参数a,用作对象的键名,第二个参数number是a的键值类型。
参数Keys可以是联合类型,这时会依次展开为多个键。
// { a: number, b: number } type T = Record<'a'|'b', number>;
上面示例中,第一个参数是联合类型'a'|'b',展开成两个键名a和b。
如果参数Type是联合类型,就表明键值是联合类型。
// { a: number|string } type T = Record<'a', number|string>;
参数Keys的类型必须兼容string|number|symbol,否则不能用作键名,会报错。
15. Required<Type>
Required<Type>返回一个新类型,将参数类型Type的所有属性变为必选属性。它与Partial<Type>的作用正好相反。
interface A { x?: number; y: number; } type T = Required<A>; // { x: number; y: number; }
16. ReadonlyArray<Type>
ReadonlyArray<Type>用来生成一个只读数组类型,类型参数Type表示数组成员的类型。
const values: ReadonlyArray<string> = ['a', 'b', 'c']; values[0] = 'x'; // 报错 values.push('x'); // 报错 values.pop(); // 报错 values.splice(1, 1); // 报错
17. ReturnType<Type>
ReturnType<Type>提取函数类型Type的返回值类型,作为一个新类型返回。
type T1 = ReturnType<() => string>; // string type T2 = ReturnType<() => { a: string; b: number }>; // { a: string; b: number }
18. ThisParameterType<Type>
ThisParameterType<Type>提取函数类型中this参数的类型。
function toHex(this: Number) { return this.toString(16); } type T = ThisParameterType<typeof toHex>; // number
如果函数没有this参数,则返回unknown。
19. 字符串类型工具
Uppercase<StringType>将字符串类型的每个字符转为大写。
type A = 'hello'; // "HELLO" type B = Uppercase<A>;
Lowercase<StringType>将字符串的每个字符转为小写。
Capitalize<StringType>将字符串的第一个字符转为大写。
Uncapitalize<StringType> 将字符串的第一个字符转为小写。
类型工具的实现
Awaited<Type>的实现如下。
type Awaited<T> = T extends null | undefined ? T : T extends object & { then( onfulfilled: infer F, ...args: infer _ ): any; } ? F extends ( value: infer V, ...args: infer _ ) => any ? Awaited<...> : never: T;
ConstructorParameters<Type>的实现如下。
type ConstructorParameters< T extends abstract new (...args: any) => any > = T extends abstract new (...args: infer P) => any ? P : never
Exclude<UnionType, ExcludedMembers>的实现如下。
type Exclude<T, U> = T extends U ? never : T;
上面代码中,等号右边的部分,表示先判断T是否兼容U,如果是的就返回never类型,否则返回当前类型T。由于never类型是任何其他类型的子类型,它跟其他类型组成联合类型时,可以直接将never类型从联合类型中“消掉”,因此Exclude<T, U>就相当于删除兼容的类型,剩下不兼容的类型。
Extract<UnionType, Union>的实现如下。
type Extract<T, U> = T extends U ? T : never;
InstanceType<Type>的实现如下。
type InstanceType< T extends abstract new (...args:any) => any > = T extends abstract new (...args: any) => infer R ? R : any;
NonNullable<Type>的实现如下。
type NonNullable<T> = T & {}
上面代码中,T & {}等同于求T & Object的交叉类型。由于 TypeScript 的非空值都属于Object的子类型,所以会返回自身;而null和undefined不属于Object,会返回never类型。
Omit<Type, Keys>的实现如下。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
OmitThisParameter<Type>的实现如下。
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;
Parameters<Type>的实现如下。
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
Partial<Type>的实现如下。
type Partial<T> = { [P in keyof T]?: T[P]; };
Pick<Type, Keys>的实现如下。
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
ReadOnly<Type>的实现如下。
type Readonly<T> = { +readonly [P in keyof T]: T[P]; };
Record<Keys, Type>的实现如下。
type Record<K extends string|number|symbol, T> = { [P in K]: T; }
Required<Type>的实现如下。
type Required<T> = { [P in keyof T]-?: T[P]; };
ReadonlyArray<Type>的实现如下。
interface ReadonlyArray<T> { readonly length: number; readonly [n: number]: T; // ... }
ReturnType<Type>的实现如下。
type ReturnType< T extends (...args: any) => any > = T extends (...args: any) => infer R ? R : any;
ThisParameterType<Type>的实现如下。
type ThisParameterType<T> = T extends ( this: infer U, ...args: never ) => any ? U : unknown;
tsconfig.json
https://wangdoc.com/typescript/tsconfig.json
tsconfig.json是 TypeScript 项目的配置文件,放在项目的根目录。反过来说,如果一个目录里面有tsconfig.json,TypeScript 就认为这是项目的根目录。
tsconfig.json文件可以不必手写,使用 tsc 命令的--init参数自动生成。
$ tsc --init