上篇介绍了typescript的基本用法以及一些基本的知识点,这篇文章将介绍更深入细节一点的typescript知识
函数
局部类型
函数内部允许声明其他类型,该类型只在函数内部有效,称为局部类型。
function hello(txt:string) { type message = string; let newTxt:message = 'hello ' + txt; return newTxt; } const newTxt:message = hello('world'); // 报错
上面示例中,类型message
是在函数hello()
内部定义的,只能在函数内部使用。在函数外部使用,就会报错。
高阶函数
一个函数的返回值还是一个函数,那么前一个函数就称为高阶函数(higher-order function)
(a: number) => (b: number) => a * b
函数重载
有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载(function overload)
reverse('abc') // 'cba' reverse([1, 2, 3]) // [3, 2, 1] //参数可以是字符串,也可以是数组。
该函数内部有处理字符串和数组的两套逻辑,根据参数类型的不同,分别执行对应的逻辑。这就叫“函数重载”。
TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型。
function reverse(str:string):string; function reverse(arr:any[]):any[];
构造函数
构造函数的最大特点,就是必须使用new
命令调用。
const d = new Date();
某些函数既是构造函数,又可以当作普通函数使用,比如Date()
。这时,类型声明可以写成下面这样。
type F = { new (s:string): object; (n?:number): number; }
泛型使用注意
尽量少用泛型
泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。
类型参数越少越好
function filter< T, Fn extends (arg:T) => boolean >( arr:T[], func:Fn ): T[] { return arr.filter(func); }
上面示例有两个类型参数,但是第二个类型参数Fn
是不必要的,完全可以直接写在函数参数的类型声明里面。
function filter<T>( arr:T[], func:(arg:T) => boolean ): T[] { return arr.filter(func); }
类型参数需要出现两次
如果类型参数在定义后只出现一次,那么很可能是不必要的。也就是说,只有当类型参数用到两次或两次以上,才是泛型的适用场合。
泛型可以嵌套
type OrNull<T> = T|null; type OneOrMany<T> = T|T[]; type OneOrManyOrNull<T> = OrNull<OneOrMany<T>>;
上面示例中,最后一行的泛型OrNull
的类型参数,就是另一个泛型OneOrMany
。
Enum 类型
Enum 是 TypeScript 新增的一种数据结构和类型,中文译为“枚举”
实际开发中可能会定义一些常量,例如颜色RED、GREEN、BLUE。TypeScript 就设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用。
enum Color { Red, // 0 Green, // 1 Blue // 2 }
上面示例声明了一个 Enum 结构Color
,里面包含三个成员Red
、Green
和Blue
。第一个成员的值默认为整数0
,第二个为1
,第二个为2
,以此类推。
使用时,调用 Enum 的某个成员,与调用对象属性的写法一样,可以使用点运算符,也可以使用方括号运算符。
let c = Color.Green; // 1 // 等同于 let c = Color['Green']; // 1
Enum 结构本身也是一种类型。比如,上例的变量c
等于1
,它的类型可以是 Color,也可以是number
。
let c:Color = Color.Green; // 正确 let c:number = Color.Green; // 正确
Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。
// 编译前 enum Color { Red, // 0 Green, // 1 Blue // 2 } // 编译后 let Color = { Red: 0, Green: 1, Blue: 2 };
由于 TypeScript 的定位是 JavaScript 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。
Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。
缺点
Enum 作为类型有一个缺点,就是输入任何数值都不报错。
enum Bool { No, Yes } function foo(noYes:Bool) { // ... } foo(33); // 不报错
上面代码中,函数foo
的参数noYes
只有两个可用的值,但是输入任意数值,编译都不会报错。
由于 Enum 结构编译后是一个对象,所以不能有与它同名的变量(包括对象、函数、类等)
成员的值
Enum 成员默认不必赋值,系统会从零开始逐一递增,按照顺序为每个成员赋值,比如0、1、2……
enum Color { Red, Green, Blue } // 等同于 enum Color { Red = 0, Green = 1, Blue = 2 }
成员的值可以是任意数值,但不能是大整数(Bigint)。
enum Color { Red = 90, Green = 0.5, Blue = 7n // 报错 }
成员的值甚至可以相同。
enum Color { Red = 0, Green = 0, Blue = 0 }
如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。
enum Color { Red = 7, Green, // 8 Blue // 9 } // 或者 enum Color { Red, // 0 Green = 7, Blue // 8 }
Enum 成员值都是只读的,不能重新赋值。了让这一点更醒目,通常会在 enum 关键字前面加上const
修饰,表示这是常量,不能再次赋值。
const enum Color { Red, Green, Blue }
加上const
还有一个好处,就是编译为 JavaScript 代码后,代码中 Enum 成员会被替换成对应的值,这样能提高性能表现。
const enum Color { Red, Green, Blue } const x = Color.Red; const y = Color.Green; const z = Color.Blue; // 编译后 const x = 0 /* Color.Red */; const y = 1 /* Color.Green */; const z = 2 /* Color.Blue */;
上面示例中,由于 Enum 结构前面加了const
关键字,所以编译产物里面就没有生成对应的对象,而是把所有 Enum 成员出现的场合,都替换成对应的常量。
同名 Enum 的合并
多个同名的 Enum 结构会自动合并。
enum Foo { A, } enum Foo { B = 1, } enum Foo { C = 2, } // 等同于 enum Foo { A, B = 1, C = 2 }
Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。
enum Foo { A, } enum Foo { B, // 报错 } // 上面示例中,Foo的两段定义的第一个成员,都没有设置初始值,导致报错。
同名 Enum 合并时,不能有同名成员,否则报错。
enum Foo { A, B } enum Foo { B = 1, // 报错 C }
同名 Enum 合并的另一个限制是,所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。
// 正确 enum E { A, } enum E { B = 1, } // 正确 const enum E { A, } const enum E { B = 1, } // 报错 enum E { A, } const enum E2 { B = 1, }
同名 Enum 的合并,最大用处就是补充外部定义的 Enum 结构。
字符串 Enum
Enum 成员的值除了设为数值,还可以设为字符串。也就是说,Enum 也可以用作一组相关字符串的集合。
enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT', }
注意:字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。
enum Foo { A, // 0 B = 'hello', C // 报错 }
A
之前没有其他成员,所以可以不设置初始值,默认等于0
;C
之前有一个字符串成员,必须C
必须有初始值,不赋值就报错了。
Enum 成员可以是字符串和数值混合赋值。除了数值和字符串,Enum 成员不允许使用其他值(比如 Symbol 值)。
变量类型如果是字符串 Enum,就不能再赋值为字符串,这跟数值 Enum 不一样。
enum MyEnum { One = 'One', Two = 'Two', } let s = MyEnum.One; s = 'One'; // 报错,变量s的类型是MyEnum,再赋值为字符串就报错。
由于这个原因,如果函数的参数类型是字符串 Enum,传参时就不能直接传入字符串,而要传入 Enum 成员。
enum MyEnum { One = 'One', Two = 'Two', } function f(arg:MyEnum) { return 'arg is ' + arg; } f('One') // 报错
字符串 Enum 作为一种类型,有限定函数参数的作用
字符串 Enum 可以使用联合类型(union)代替。
function move( where:'Up'|'Down'|'Left'|'Right' ) { // ... }
上面示例中,函数参数where
属于联合类型,效果跟指定为字符串 Enum 是一样的。
注意,字符串 Enum 的成员值,不能使用表达式赋值。
enum MyEnum { A = 'one', B = ['T', 'w', 'o'].join('') // 报错 }
keyof 运算符
keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。
enum MyEnum { A = 'a', B = 'b' } // 'A'|'B' type Foo = keyof typeof MyEnum;
上面示例中,keyof typeof MyEnum
可以取出MyEnum
的所有成员名,所以类型Foo
等同于联合类型'A'|'B'
。
注意,这里的typeof
是必需的,否则keyof MyEnum
相当于keyof number
。
type Foo = keyof MyEnum; // "toString" | "toFixed" | "toExponential" | // "toPrecision" | "valueOf" | "toLocaleString"
上面示例中,类型Foo
等于类型number
的所有原生属性名组成的联合类型。
这是因为 Enum 作为类型,本质上属于number
或string
的一种变体,而typeof MyEnum
会将MyEnum
当作一个值处理,从而先其转为对象类型,就可以再用keyof
运算符返回该对象的所有属性名。
如果要返回 Enum 所有的成员值,可以使用in
运算符。
enum MyEnum { A = 'a', B = 'b' } // { a:any, b: any } type Foo = { [key in MyEnum]: any };
反向映射
数值 Enum 存在反向映射,即可以通过成员值获得成员名。
enum Weekdays { Monday = 1, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } console.log(Weekdays[3]) // Wednesday
上面示例中,Enum 成员Wednesday
的值等于3,从而可以从成员值3
取到对应的成员名Wednesday
,这就叫反向映射。
这是因为 TypeScript 会将上面的 Enum 结构,编译成下面的 JavaScript 代码。
var Weekdays; (function (Weekdays) { Weekdays[Weekdays["Monday"] = 1] = "Monday"; Weekdays[Weekdays["Tuesday"] = 2] = "Tuesday"; Weekdays[Weekdays["Wednesday"] = 3] = "Wednesday"; Weekdays[Weekdays["Thursday"] = 4] = "Thursday"; Weekdays[Weekdays["Friday"] = 5] = "Friday"; Weekdays[Weekdays["Saturday"] = 6] = "Saturday"; Weekdays[Weekdays["Sunday"] = 7] = "Sunday"; })(Weekdays || (Weekdays = {}));
上面代码中,实际进行了两组赋值,以第一个成员为例。
Weekdays[ Weekdays["Monday"] = 1 ] = "Monday"; // 上面代码有两个赋值运算符(=),实际上等同于下面的代码。 Weekdays["Monday"] = 1; Weekdays[1] = "Monday";
注意,这种情况只发生在数值 Enum,对于字符串 Enum,不存在反向映射。这是因为字符串 Enum 编译后只有一组赋值。
类型断言
对于没有类型声明的值,TypeScript 会进行类型推断,很多时候得到的结果,未必是开发者想要的。
type T = 'a'|'b'|'c'; let foo = 'a'; let bar:T = foo; // 报错
上面示例中,最后一行报错,原因是 TypeScript 推断变量foo
的类型是string
,而变量bar
的类型是'a'|'b'|'c'
,前者是后者的父类型。父类型不能赋值给子类型,所以就报错了。
TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。
这种做法的实质是,允许开发者在某个位置“绕过”编译器的类型推断,让本来通不过类型检查的代码能够通过,避免编译器报错。这样虽然削弱了 TypeScript 类型系统的严格性,但是为开发者带来了方便,毕竟开发者比编译器更了解自己的代码。
type T = 'a'|'b'|'c'; let foo = 'a'; let bar:T = foo as T; // 正确
类型断言有两种语法。
// 语法一:<类型>值 <Type>value // 语法二:值 as 类型 value as Type
上面两种语法是等价的,value
表示值,Type
表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。
// 语法一 let bar:T = <T>foo; // 语法二 let bar:T = foo as T;
类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误的隐患。
const data:object = { a: 1, b: 2, c: 3 }; data.length; // 报错 (data as Array<string>).length; // 正确
上面示例中,变量data
是一个对象,没有length
属性。但是通过类型断言,可以将它的类型断言为数组,这样使用length
属性就能通过类型检查。但是,编译后的代码在运行时依然会报错,所以类型断言可以让错误的代码通过编译。
类型断言的一大用处是,指定 unknown 类型的变量的具体类型。
类型断言的条件
类型断言并不意味着,可以把某个值断言为任意类型。
const n = 1; const m:string = n as string; // 报错
类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件。
expr as T
上面代码中,expr
是实际的值,T
是类型断言,它们必须满足下面的条件:expr
是T
的子类型,或者T
是expr
的子类型。
也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为any
类型和unknown
类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介。
// 或者写成 <T><unknown>expr expr as unknown as T
as const 断言
如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const 命令声明的变量,则被推断为值类型常量。
TypeScript 提供了一种特殊的类型断言as const
,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。
let s = 'JavaScript' as const; s = 'Python'; // 报错
使用了as const
断言以后,let 变量就不能再改变值了。
注意, as const
断言只能用于字面量,不能用于变量。
let s = 'JavaScript'; setLang(s as const); // 报错
断言函数
断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。
function isString(value) { if (typeof value !== 'string') throw new Error('Not a string'); }
上面示例中,函数isString()
就是一个断言函数,用来保证参数value
是一个字符串。
为了更清晰地表达断言函数,TypeScript 3.7 引入了新的类型写法。
function isString(value:unknown):asserts value is string { if (typeof value !== 'string') throw new Error('Not a string'); }
上面示例中,函数isString()
的返回值类型写成asserts value is string
,其中asserts
和is
都是关键词,value
是函数的参数名,string
是函数参数的预期类型。它的意思是,该函数用来断言参数value
的类型是string
,如果达不到要求,程序就会在这里中断。
装饰器
装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。
在语法上,装饰器有如下几个特征。
(1)第一个字符(或者说前缀)是@
,后面是一个表达式。
(2)@
后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
(3)这个函数接受所修饰对象的一些相关值作为参数。
(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
下面就是一个最简单的装饰器。
function simpleDecorator() { console.log('hi'); } @simpleDecorator class A {} // "hi"
上面示例中,函数simpleDecorator()
用作装饰器,附加在类A
之上,后者在代码解析时就会打印一行日志。
装饰器有多种形式,基本上只要在@
符号后面添加表达式都是可以的。下面都是合法的装饰器。
@myFunc @myFuncFactory(arg1, arg2) @libraryModule.prop @someObj.method(123) @(wrap(dict['prop']))
类装饰器
类装饰器的类型描述如下。
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()
方法,实例就可以直接使用该方法。
方法装饰器
方法装饰器用来装饰类的方法(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);
如果方法装饰器返回一个新的函数,就会替代所装饰的原始函数。
function replaceMethod() { return function () { return `How are you, ${this.name}?`; } } class Person { constructor(name) { this.name = name; } @replaceMethod hello() { return `Hi ${this.name}!`; } } const robin = new Person('Robin'); robin.hello() // 'How are you, Robin?'
属性装饰器
属性装饰器用来装饰定义在类顶部的属性(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"
属性装饰器的返回值函数,可以用来更改属性的初始值。
function twice() { return initialValue => initialValue * 2; } class C { @twice field = 3; } const inst = new C(); inst.field // 6
getter 装饰器,setter 装饰器
getter 装饰器和 setter 装饰器,是分别针对类的取值器(getter)和存值器(setter)的装饰器。它们的类型描述如下。
type ClassGetterDecorator = ( value: Function, context: { kind: 'getter'; name: string | symbol; static: boolean; private: boolean; access: { get: () => unknown }; addInitializer(initializer: () => void): void; } ) => Function | void; type ClassSetterDecorator = ( value: Function, context: { kind: 'setter'; name: string | symbol; static: boolean; private: boolean; access: { set: (value: unknown) => void }; addInitializer(initializer: () => void): void; } ) => Function | void;
注意,getter 装饰器的上下文对象context
的access
属性,只包含get()
方法;setter 装饰器的access
属性,只包含set()
方法。
这两个装饰器要么不返回值,要么返回一个函数,取代原来的取值器或存值器。
class C { @lazy get value() { console.log('正在计算……'); return '开销大的计算结果'; } } function lazy( value:any, {kind, name}:any ) { if (kind === 'getter') { return function (this:any) { const result = value.call(this); Object.defineProperty( this, name, { value: result, writable: false, } ); return result; }; } return; } const inst = new C(); inst.value // 正在计算…… // '开销大的计算结果' inst.value // '开销大的计算结果'
上面示例中,第一次读取inst.value
,会进行计算,然后装饰器@lazy
将结果存入只读属性value
,后面再读取这个属性,就不会进行计算了。
accessor 装饰器
装饰器语法引入了一个新的属性修饰符accessor
。
class C { accessor x = 1; }
上面示例中,accessor
修饰符等同于为属性x
自动生成取值器和存值器,它们作用于私有属性x
。也就是说,上面的代码等同于下面的代码。
class C { #x = 1; get x() { return this.#x; } set x(val) { this.#x = val; } }
//accessor 装饰器的类型如下。 type ClassAutoAccessorDecorator = ( value: { get: () => unknown; set(value: unknown) => void; }, context: { kind: "accessor"; name: string | symbol; access: { get(): unknown, set(value: unknown): void }; static: boolean; private: boolean; addInitializer(initializer: () => void): void; } ) => { get?: () => unknown; set?: (value: unknown) => void; init?: (initialValue: unknown) => unknown; } | void;
accessor 装饰器的value
参数,是一个包含get()
方法和set()
方法的对象。该装饰器可以不返回值,或者返回一个新的对象,用来取代原来的get()
方法和set()
方法。此外,装饰器返回的对象还可以包括一个init()
方法,用来改变私有属性的初始值。
下面是一个例子
class C { @logged accessor x = 1; } function logged(value, { kind, name }) { if (kind === "accessor") { let { get, set } = value; return { get() { console.log(`getting ${name}`); return get.call(this); }, set(val) { console.log(`setting ${name} to ${val}`); return set.call(this, val); }, init(initialValue) { console.log(`initializing ${name} with value ${initialValue}`); return initialValue; } }; } } let c = new C(); c.x; // getting x c.x = 123; // setting x to 123
装饰器@logged
为属性x
的存值器和取值器,加上了日志输出。
装饰器的执行顺序
装饰器的执行分为两个阶段。
(1)评估(evaluation):计算@
符号后面的表达式的值,得到的应该是函数。
(2)应用(application):将评估装饰器后得到的函数,应用于所装饰对象。
也就是说,装饰器的执行顺序是,先评估所有装饰器表达式的值,再将其应用于当前类。
应用装饰器时,顺序依次为方法装饰器和属性装饰器,然后是类装饰器。
请看下面例子
function d(str:string) { console.log(`评估 @d(): ${str}`); return ( value:any, context:any ) => console.log(`应用 @d(): ${str}`); } function log(str:string) { console.log(str); return str; } @d('类装饰器') class T { @d('静态属性装饰器') static staticField = log('静态属性值'); @d('原型方法') [log('计算方法名')]() {} @d('实例属性') instanceField = log('实例属性值'); }
上面示例中,类T
有四种装饰器:类装饰器、静态属性装饰器、方法装饰器、属性装饰器。它的运行结果如下。
// "评估 @d(): 类装饰器" // "评估 @d(): 静态属性装饰器" // "评估 @d(): 原型方法" // "计算方法名" // "评估 @d(): 实例属性" // "应用 @d(): 原型方法" // "应用 @d(): 静态属性装饰器" // "应用 @d(): 实例属性" // "应用 @d(): 类装饰器" // "静态属性值"
可以看到,类载入的时候,代码按照以下顺序执行。
(1)装饰器评估:这一步计算装饰器的值,首先是类装饰器,然后是类内部的装饰器,按照它们出现的顺序。
注意,如果属性名或方法名是计算值(本例是“计算方法名”),则它们在对应的装饰器评估之后,也会进行自身的评估。
(2)装饰器应用:实际执行装饰器函数,将它们与对应的方法和属性进行结合。
原型方法的装饰器首先应用,然后是静态属性和静态方法装饰器,接下来是实例属性装饰器,最后是类装饰器。
注意,“实例属性值”在类初始化的阶段并不执行,直到类实例化时才会执行。
如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。
class Person { name: string; constructor(name: string) { this.name = name; } @bound @log greet() { console.log(`Hello, my name is ${this.name}.`); } }
上面示例中,greet()
有两个装饰器,内层的@log
先执行,外层的@bound
针对得到的结果再执行。
类型运算
改变成员类型的顺序不影响联合类型的结果类型。
type T0 = string | number; type T1 = number | string;
对部分类型成员使用分组运算符不影响联合类型的结果类型。
type T0 = (boolean | string) | number; type T1 = boolean | (string | number);
联合类型的成员类型可以进行化简。假设有联合类型“U = T0 | T1”,如果T1是T0的子类型,那么可以将类型成员T1从联合类型U中消去。最后,联合类型U的结果类型为“U = T0”。例如,有联合类型“boolean | true | false”。其中,true类型和false类型是boolean类型的子类型,因此可以将true类型和false类型从联合类型中消去。最终,联合类型“boolean | true | false”的结果类型为boolean类型。
type T0 = boolean | true | false; // 所以T0等同于 T1 type T1 = boolean;
优先级
&
的优先级高于|
A & B | C & D // 该类型等同于如下类型: (A & B) | (C & D)
分配律
A & (B | C) // 等同于 (A & B) | (A & C)
(A | B) & (C | D) ≡ A & C | A & D | B & C | B & D
never
never 可以视为空集。
type NeverIntersection = never & string; // Type: never type NeverUnion = never | string; // Type: string
很适合在交叉类型中用作过滤。
type OnlyStrings<T> = T extends string ? T : never; type RedOrBlue = OnlyStrings<"red" | "blue" | 0 | false>; // Equivalent to: "red" | "blue"
类型映射
映射(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
。
键名重映射
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; };
上面示例中,类型B
是类型A
的映射,但在映射时把属性名改掉了,在原始属性名后面加上了字符串ID
。
下面是另一个例子。
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; }
上面示例中,类型LazyPerson
是类型Person
的映射,并且把键名改掉了。
总结
这篇文章主要是对上一篇文章的补充,介绍了更多typescript中细节的内容,这些内容在日常中可能用的并不是很频繁,但是学习了解一下也是十分有意思的,拓宽自己的知识面。最后也是感谢大家看到这里,麻烦点个赞!发大财!