1.TypeScript 概念
TypeScript(简称 TS,静态类型)是微软公司开发的一种基于 JavaScript (简称 JS,动态类型)语言的编程语言。TypeScript 可以看成是 JavaScript 的超集(superset),即它继承了后者的全部语法,增加了一些自己的语法。使用TypeScript可以帮助开发人员在编码过程中避免一些常见的错误,并提供更好的代码编辑功能和工具支持。
2.数据类型
基础类型
number
: 表示数字,包括整数和浮点数。bigint
: 表示大整数。string
: 表示文本字符串。boolean
: 表示布尔值。null、undefined
: 分别表示null和undefined。symbol
: 表示唯一的、不可变的值。
复合类型
array
: 表示数组,可以使用number[]
或Array<number>
来声明其中元素的类型。tuple
: 表示元组,用于表示固定数量和类型的数组。enum
: 表示枚举类型,用于定义具名常量集合。
对象类型
object
: 表示非原始类型,即除number、string、boolean、symbol、null或undefined之外的类型。interface
: 用于描述对象的结构,并且可以重复使用。
函数类型
function
: 表示函数类型。void
: 表示函数没有返回值。
高级类型
union types
: 联合类型,表示一个值可以是几种类型之一。intersection types
: 交叉类型,表示一个值同时拥有多种类型的特性。
顶层类型
any
: 表示任意类型。unknown
: 严格版的 any 类型。
底层类型
never
: 类型表示的是那些永不存在的值的类型。
3.基础用法
类型声明
变量只有赋值后才能使用,否则就会报错
let x: number;
console.log(x); // 报错
只读数组
const arr: readonly number[] = [0, 1];// TypeScript 提供了两个专门的泛型,用来生成只读数组的类型
const a1: ReadonlyArray<number> = [0, 1];
const a2: Readonly<number[]> = [0, 1];
只读对象
// 方式一
const myUser: {readonly name: string;
} = {name: 'Sabrina',
};// 方式二
const myUser = {name: 'Sabrina',
} as const;
元组
元组表示成员类型可以自由设置的数组,即数组的各个成员的类型可以不同。
// 数组
let a: number[] = [1];// 元组
let t: [number] = [1];// 元组成员的类型可以添加问号后缀(?),表示该成员是可选的
let a: [number, number?] = [1];// 使用扩展运算符(...),可以表示不限成员数量的元组
type NamedNums = [string, ...number[]];
const a: NamedNums = ['A', 1, 2];
只读元组
// 写法一
type t = readonly [number, string];// 写法二
type t = Readonly<[number, string]>;
包装对象类型与字面量类型
'hello' // 字面量
new String('hello') // 包装对象
TypeScript 对五种原始类型分别提供了大写和小写两种类型。
- Boolean 和 boolean
- String 和 string
- Number 和 number
- BigInt 和 bigint
- Symbol 和 symbol
其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量。
const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错
建议只使用小写类型,不使用大写类型。TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。
Object 类型与 object 类型
大写的 Object 类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值(null、undefined除外),都是 Object 类型,这囊括了几乎所有的值。
另外,空对象{}
是 Object 类型的简写形式。
小写的 object 类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。
注意,无论是大写的 Object 类型,还是小写的 object 类型,都只包含 JavaScript 内置对象原生的属性和方法,用户自定义的属性和方法都不存在于这两个类型之中。
const o1:Object = { foo: 0 };
const o2:object = { foo: 0 };o1.toString() // 正确
o1.foo // 报错o2.toString() // 正确
o2.foo // 报错
值类型
TypeScript 规定,单个值也是一种类型,称为“值类型”。
let x:'hello';x = 'hello'; // 正确
x = 'world'; // 报错// 遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型
// x 的类型是 "https"
const x = 'https';
联合类型
联合类型(union types)指的是多个类型组成的一个新类型,使用符号|
表示。
let x:string|number;// 值类型相结合
let setting:true|false;// 联合类型的第一个成员前面,也可以加上竖杠|,便于多行书写
let x:| 'one'| 'two'| 'three';
如果一个变量有多种类型,读取该变量时,需要进行“类型缩小”
function printId(id:number|string
) {if (typeof id === 'string') {console.log(id.toUpperCase());} else {console.log(id);}
}
交叉类型
交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号&
表示。
// 交叉类型的主要用途是表示对象的合成
let obj: { foo: string } & { bar: string };
obj = {foo: 'hello',bar: 'world',
};// 交叉类型常常用来为对象类型添加新属性
type A = { foo: number };
type B = A & { bar: number };
type 命令
type
命令用来定义一个类型的别名。
type Age = number;
别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便
别名不允许重名。
type Color = 'red';
type Color = 'blue'; // 报错
别名的作用域是块级作用域。这意味着,代码块内部定义的别名,影响不到外部。
type Color = 'red';
if (Math.random() < 0.5) {type Color = 'blue';
}
别名支持使用表达式。
type World = 'world';
type Greeting = `hello ${World}`;
typeof 运算符
TypeScript 中的 typeof
运算符与 JavaScript 不同,其返回值是该值的 TypeScript 类型。
const a = { x: 0 };
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number// typeof 的参数只能是标识符,不能是需要运算的表达式
type T = typeof Date(); // 报错// typeof 命令的参数不能是类型
type Age = number;
type MyAge = typeof Age; // 报错
函数重载
有些函数可以接受不同类型或不同个数的参数,并且根据参数的不同,会有不同的函数行为。这种根据参数类型不同,执行不同逻辑的行为,称为函数重载。
TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型。
function reverse(str: string): string;
function reverse(arr: any[]): any[];// 注意,重载的各个类型描述与函数的具体实现之间,不能有其他代码,否则报错
// 另外,类型最宽的声明应该放在最后面
构造函数
class Animal {numLegs: number = 4;
}type AnimalConstructor = new () => Animal;function create(c: AnimalConstructor): Animal {return new c();
}const a = create(Animal);
另一种类型写法,采用对象形式
type F = {new (s: string): object;
};// 某些函数既是构造函数,又可以当作普通函数使用
type F = {new (s: string): object;(n?: number): number;
};
4.接口 interface
interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。
简介
(1)对象属性
interface IObj {a: number;b?: number; // 可选readonly c: number; // 只读
}
(2)对象的属性索引
interface MyObj {[prop: string]: number;a: boolean; // 报错, 不能为 boolean[prop: number]: string; // 报错, 不能为 string, 与 number 冲突[prop: number]: number; // 正确, 数值属性名会自动转换成字符串属性名
}// 属性的数值索引,可以指定数组的类型
interface A {[prop: number]: string;
}
const obj: A = ['a', 'b', 'c'];
(3)对象的方法
// 写法一
interface A {f(x: boolean): string;
}// 写法二
interface B {f: (x: boolean) => string;
}// 写法三
interface C {f: { (x: boolean): string };
}// 属性名可以采用表达式
const f = 'f';
interface A {[f](x: boolean): string;
}// 类型方法可以重载
interface A {f(): number;f(x: boolean): boolean;f(x: string, y: string): string;
}
(4)函数
interface 也可以用来声明独立的函数。
interface Add {(x: number, y: number): number;
}const myAdd: Add = (x, y) => x + y;
(5)构造函数
interface 内部可以使用 new 关键字,表示构造函数。
interface ErrorConstructor {new (message?: string): Error;
}
继承
(1)interface 继承 interface
interface Style {color: string;
}interface Shape {name: string;
}// 单个继承, 这里 Circle1 是子接口,Shape 是父接口
interface Circle1 extends Shape {radius: number;
}// 多重继承
interface Circle2 extends Style, Shape {radius: number;
}// 子接口与父接口的同名属性必须是类型兼容的
interface Circle1 extends Shape {name: number; // 报错
}
// 多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突
(2)interface 继承 type
type Country = {name: string;capital: string;
};interface CountryWithPop extends Country {population: number;
}
// 注意,如果type命令定义的类型不是对象,interface 就无法继承。
(3)interface 继承 class
class A {x: string = '';y(): boolean {return true;}
}// 继承该类的所有成员
interface B extends A {z: number;
}// 某些类拥有私有(private)成员和保护(protected)成员,interface 可以继承这样的类,
// 但是意义不大,因为私有成员和保护成员只能在类内部访问,无法在类外部使用。
接口合并
多个同名接口会合并成一个接口。
interface Box {height: number;
}
interface Box {width: number;// 属性名相同时, 不能冲突height: string; // 报错, 类型冲突
}// --------------------------
// 对全局对象或者外部库,添加自己的属性和方法
interface Document {foo: string;
}
document.foo = 'hello';// --------------------------
// 同名方法有不同的类型声明,那么会发生函数重载
// 后面的定义比前面的定义具有更高的优先级
interface Cloner {clone(animal: Animal): Animal;
}
interface Cloner {clone(animal: Sheep): Sheep;
}
interface Cloner {clone(animal: Dog): Dog;clone(animal: Cat): Cat;
}
// 等同于
interface Cloner {clone(animal: Dog): Dog;clone(animal: Cat): Cat;clone(animal: Sheep): Sheep;clone(animal: Animal): Animal;
}// 例外:同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级
// 类型越具体, 优先级越高
interface A {f(x: 'foo'): boolean;
}
interface A {f(x: string): void;f(x: any): void;
}
// 等同于
interface A {f(x: 'foo'): boolean;f(x: string): void;f(x: any): void;
}// --------------------------
// 若两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型
interface Circle {area: bigint;
}
interface Rectangle {area: number;
}
declare const s: Circle | Rectangle;
s.area; // bigint | number
interface 与 type 的异同
很多对象类型既可以用 interface 表示,也可以用 type 表示。
区别有下面几点:
- (1)
type
能够表示非对象类型,而interface
只能表示对象类型(包括数组、函数等)。 - (2)
interface
可以继承其他类型,type
不支持继承。
继承的主要作用是添加属性,type 定义的对象类型如果想要添加属性,只能使用&
运算符,重新定义一个类型。
type Animal = {name: string;
};
type Bear = Animal & {honey: boolean;
};// interface 可以继承 type
type Foo = { x: number };
interface Bar extends Foo {y: number;
}// type 也可以继承 interface
interface Foo {x: number;
}
type Bar = Foo & { y: number };
- (3)同名
interface
会自动合并,同名type
则会报错。 - (4)
interface
不能包含属性映射(mapping),type
可以。
interface Point {x: number;y: number;
}
// 正确
type PointCopy1 = {[Key in keyof Point]: Point[Key];
};
// 报错
interface PointCopy2 {[Key in keyof Point]: Point[Key];
};
- (5)
this
关键字只能用于interface
。
// 正确
interface Foo {add(num: number): this;
}
// 报错
type Foo = {add(num: number): this;
};
- (6)
type
可以扩展原始数据类型,interface
不行。
// 正确
type MyStr = string & {type: 'new';
};
// 报错
interface MyStr extends string {type: 'new';
}
- (7)
interface
无法表达某些复杂类型(比如交叉类型和联合类型),但是type
可以。
type A = {/* ... */
};
type B = {/* ... */
};
type AorB = A | B; // 联合类型
// 交叉类型
type AorBwithName = AorB & {name: string;
};
5.类 Class
class Point {readonly x: number;y: number;constructor(x: number, y: number) {this.x = x; // 构造方法内部可以设置只读属性的初值this.y = y;}add(point: Point) {return new Point(this.x + point.x, this.y + point.y);}
}
存取器方法
存取器包括取值器(getter)和存值器(setter)两种方法。
class C {_name = '';get name() {return this._name;}set name(value) {this._name = value;}// 没有set方法,那么该属性自动成为只读属性
}
implements 关键字
interface Country {name: string;capital: string;
}
// 或者
type Country = {name: string;capital: string;
};
// 或者
class Country {name: string;capital: string;
}// 实现接口, 实现多个接口用逗号
class MyCountry implements Country {name = '';capital = '';
}
类与接口的合并
如果一个类和一个接口同名,那么接口会被合并进类。
class A {x: number = 1;
}interface A {y: number;
}let a = new A();
a.y = 10;a.x; // 1
a.y; // 10
Class 实例类型
可以声明类型为 Class,也可以声明类型为 Interface。
interface MotorVehicle {}class Car implements MotorVehicle {}// 写法一
const c1: Car = new Car();
// 写法二
const c2: MotorVehicle = new Car();
类的继承
类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。
class A {greet() {console.log('Hello, world!');}
}class B extends A {// 子类可以覆盖基类的同名方法,// 注意, 子类的同名方法不能与基类的类型定义相冲突, 这里参数不能写成必填参数greet(name?: string) {if (name === undefined) {super.greet(); // 调用基类方法} else {console.log(`Hello, ${name}`);}}
}const b = new B();
b.greet(); // "Hello, world!"
可访问性修饰符
类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:public
、private
和protected
。
private
修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。
protected
修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。
实例属性的简写形式
通过构造方法传入
class Point {x: number;y: number;constructor(x: number, y: number) {this.x = x;this.y = y;}
}
TypeScript 提供了一种简写形式
class Point {constructor(public x: number, public y: number) {}
}
静态成员
类的内部可以使用static
关键字,定义静态成员。
静态成员是只能通过类本身使用的成员,不能通过实例对象使用。
class MyClass {static x = 0;static printX() {console.log(MyClass.x);}
}MyClass.x; // 0
MyClass.printX(); // 0
static
关键字前面可以使用 public、private、protected 修饰符。
泛型类
class Box<Type> {contents: Type;constructor(value: Type) {this.contents = value;}
}const b: Box<string> = new Box('hello!');
注意,静态成员不能使用泛型的类型参数。
抽象类,抽象成员
在类的定义前面,加上关键字abstract
,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”。
abstract class A {id = 1;
}const a = new A(); // 报错
抽象类只能当作基类使用,用来在它的基础上定义子类。
抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”
(abstract member),即属性名和方法名有 abstract 关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。
abstract class A {abstract foo: string;bar: string = '';
}class B extends A {foo = 'b';
}
几个注意点:
-
(1)抽象成员只能存在于抽象类,不能存在于普通类。
-
(2)抽象成员不能有具体实现的代码。
-
(3)抽象成员前也不能有 private 修饰符,否则无法在子类中实现该成员。
-
(4)一个子类最多只能继承一个抽象类。
6.泛型
泛型的特点就是带有“类型参数”(type parameter)。
// <T>,就是类型参数
function getFirst<T>(arr: T[]): T {return arr[0];
}
函数的泛型写法
function id<T>(arg: T): T {return arg;
}// 变量形式定义的函数
// 写法一
let myId: <T>(arg: T) => T = id;// 写法二
let myId: { <T>(arg: T): T } = id;
接口的泛型写法
interface Box<Type> {contents: Type;
}let box: Box<string>;// 泛型接口还有第二种写法
interface Fn {<Type>(arg: Type): Type;
}function id<Type>(arg: Type): Type {return arg;
}let myId: Fn = id;
类的泛型写法
class Pair<K, V> {key: K;value: V;
}
类型别名的泛型写法
type Nullable<T> = T | undefined | null;type Container<T> = { value: T };
类型参数的默认值
function getFirst<T = string>(arr: T[]): T {return arr[0];
}
数组的泛型表示
数组类型有一种表示方法是Array<T>
。ReadonlyArray<T>
接口,表示只读数组。
let arr: Array<number> = [1, 2, 3];let arr: ReadonlyArray<number> = [1, 2, 3];
类型参数的约束条件
// 约束参数必须有 length 属性
function comp<T extends { length: number }>(a: T, b: T) {if (a.length >= b.length) {return a;}return b;
}comp([1, 2], [1, 2, 3]); // 正确
comp('ab', 'abc'); // 正确
comp(1, 2); // 报错
类型参数的约束条件采用下面的形式。
<TypeParameter extends ConstraintType>
类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。
type Fn<A extends string, B extends string = 'world'> = [A, B];type Result = Fn<'hello'>; // ["hello", "world"]
7.枚举 Enum
简介
Enum 结构,用来将相关常量放在一个容器里面,方便使用。
enum Color {Red, // 0Green, // 1Blue, // 2
}
// 第一个成员的值默认为整数0,第二个为1,第三个为2,以此类推。let c = Color.Green; // 1
// 等同于
let c = Color['Green']; // 1// 类型可以是 Color,也可以是number。
let c: Color = Color.Green; // 正确
let c: number = Color.Green; // 正确// Enum 既是一种类型,也是一个值
// 编译后
let Color = {Red: 0,Green: 1,Blue: 2,
};
Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。
Enum 成员的值
可以为 Enum 成员显式赋值。
// 如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。
enum Color {Red = 7,Green, // 8Blue, // 9
}// 或者
enum Color {Red, // 0Green = 7,Blue, // 8
}
Enum 成员值都是只读的,不能重新赋值。
enum Color {Red,Green,Blue,
}Color.Red = 4; // 报错
在 enum 关键字前面加上 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 的合并
多个同名的 Enum 结构会自动合并。
Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。
enum Foo {A,
}enum Foo {B, // 报错
}
不能有同名成员,否则报错。
enum Foo {A,B,
}enum Foo {B = 1, // 报错C,
}
同名 Enum 合并的另一个限制是,所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。
// 报错
enum E {A,
}
const enum E {B = 1,
}
字符串 Enum
enum Direction {Up = 'UP',Down = 'DOWN',Left = 'LEFT',Right = 'RIGHT',
}
字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。
enum Foo {A, // 0B = 'hello',C, // 报错
}
除了数值和字符串,Enum 成员不允许使用其他值(比如 Symbol 值)。
keyof 运算符
keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。
enum MyEnum {A = 'a',B = 'b',
}// 'A'|'B'
type Foo = keyof typeof MyEnum;// 返回 Enum 所有的成员值,可以使用in运算符
// { 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
8.类型断言
简介
type T = 'a' | 'b' | 'c';let foo = 'a';
let bar: T = foo as T; // 正确
类型断言有两种语法。
// 语法一:<类型>值
<Type>value;// 语法二:值 as 类型
value as Type;// 语法一因为跟 JSX 语法冲突,现在一般都使用语法二
类型断言的条件
const n = 1;
const m: string = n as string; // 报错
类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件:
expr as T;
// expr 是 T 的子类型,或者 T 是 expr 的子类型
如果真的要断言成一个完全无关的类型,需要连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为 any 类型和 unknown 类型是所有其他类型的父类型。
// 或者写成 <T><unknown>expr
expr as unknown as T;
as const 断言
let s = 'JavaScript'; // string 类型type Lang = 'JavaScript' | 'TypeScript' | 'Python'; // 联合类型function setLang(language: Lang) {/* ... */
}setLang(s); // 报错, string 类型(父类型)不能赋值给 Lang 类型(子类型)
报错的解决方法:
// 方法一:
const s = 'JavaScript';// 方法二:
let s = 'JavaScript' as const;
// 使用了 as const 断言以后,let 变量就不能再改变值了
注意,as const 断言只能用于字面量,不能用于变量。
let s = 'JavaScript';
setLang(s as const); // 报错// 另外,as const也不能用于表达式
let s = ('Java' + 'Script') as const; // 报错
as const 也可以写成前置的形式。
// 后置形式
expr as const// 前置形式
<const>expr
as const 断言可以用于整个对象,也可以用于对象的单个属性
const v1 = {x: 1,y: 2,
}; // 类型是 { x: number; y: number; }const v2 = {x: 1 as const,y: 2,
}; // 类型是 { x: 1; y: number; }const v3 = {x: 1,y: 2,
} as const; // 类型是 { readonly x: 1; readonly y: 2; }// 数组
// a1 的类型推断为 number[]
const a1 = [1, 2, 3];// a2 的类型推断为 readonly [1, 2, 3]
const a2 = [1, 2, 3] as const;
由于 as const 会将数组变成只读元组,所以很适合用于函数的 rest 参数
function add(x: number, y: number) {return x + y;
}const nums = [1, 2];
const total = add(...nums); // 报错// 修改后
const nums = [1, 2] as const;
const total = add(...nums); // 正确
Enum 成员也可以使用 as const 断言。
enum Foo {X,Y,
}
let e1 = Foo.X; // Foo
let e2 = Foo.X as const; // Foo.X
非空断言
对于那些可能为空的变量(即可能等于 undefined 或 null),可以使用 !
符号,保证这些变量不会为空。
const root = document.getElementById('root'); // 可能为 null// 报错
root.addEventListener('click', (e) => {/* ... */
});// 修改后
const root = document.getElementById('root')!; // 不可能为 null
断言函数
function isString(value: unknown): asserts value is string {if (typeof value !== 'string') throw new Error('Not a string');
}
其中asserts
和is
都是关键词,value
是函数的参数名,string
是函数参数的预期类型
9.模块
简介
TypeScript 模块除了支持所有 ES 模块的语法,特别之处在于允许输出和输入类型。
// a.ts
export type Bool = true | false;// 也可以写成两行
type Bool = true | false;
export { Bool };// b.ts
// 再另一个文件中引入
import { Bool } from './a';
let foo: Bool = true;
import type 语句
// a.ts
export interface A {foo: string;
}export let a = 123;// b.ts
import { A, a } from './a'; // A 是类型,a 是正常接口
上面写法的问题是,不利于区分类型和正常接口。有两种方法解决:
// 方法一
import { type A, a } from './a';// 方法二, import type 只能输出类型,不能输出正常接口
import type { A } from './a';// 输入默认类型
import type DefaultType from 'moduleA';
// 输入所有类型
import type * as TypeNS from 'moduleA';
CommonJS 模块
使用import =
语句和require()
命令输入了一个 CommonJS 模块
import fs = require('fs');// 或者
import * as fs from 'fs';
10.装饰器
简介
装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。
装饰器有如下几个特征:
- (1)第一个字符(或者说前缀)是
@
,后面是一个表达式。 - (2)
@
后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。 - (3)这个函数接受所修饰对象的一些相关值作为参数。
- (4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。
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"
函数simpleDecorator()
用作装饰器,附加在类A
之上,后者在代码解析时就会打印一行日志。
装饰器一般只用来为类添加某种特定行为。
@frozen
class Foo {@configurable(false)@enumerable(true)method() {}@throttle(500)expensiveMethod() {}
}
上面示例中,一共有四个装饰器,一个用在类本身(@frozen
),另外三个用在类的方法(@configurable
、@enumerable
、@throttle
)。
装饰器的结构
装饰器函数的类型定义如下。
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
,描述这个对象。
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;
方法装饰器
方法装饰器的类型描述如下。
type ClassMethodDecorator = (value: Function,context: {kind: 'method';name: string | symbol;static: boolean;private: boolean;access: { get: () => unknown };addInitializer(initializer: () => void): void;}
) => Function | void;
属性装饰器
类型描述如下
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;
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;
11.declare
简介
declare
关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。
declare 关键字可以描述以下类型。
- 变量(const、let、var 命令声明)
- type 或者 interface 命令声明的类型
- class
- enum
- 函数(function)
- 模块(module)
- 命名空间(namespace)
declare variable
declare 关键字可以给出外部变量的类型描述。
其他脚本定义的全局变量x
,使用 declare 命令给出它的类型。
declare let x:number;
x = 1; // 声明类型后使用就不会再报错
declare function
declare 关键字可以给出外部函数的类型描述。
declare function sayHello(name: string): void;sayHello('张三');
declare class
declare class Animal {constructor(name: string);eat(): void;sleep(): void;
}
declare module,declare namespace
如果想把变量、函数、类组织在一起,可以将 declare 与 module 或 namespace 一起使用。
declare namespace AnimalLib {class Animal {constructor(name: string);eat(): void;sleep(): void;}type Animals = 'Fish' | 'Dog';
}// 或者
declare module AnimalLib {class Animal {constructor(name: string);eat(): void;sleep(): void;}type Animals = 'Fish' | 'Dog';
}
declare module 和 declare namespace 里面,加不加 export 关键字都可以。
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 enum
declare enum E1 {A,B,
}declare enum E2 {A = 0,B = 1,
}declare const enum E3 {A,B,
}declare const enum E4 {A = 0,B = 1,
}
12. d.ts 类型声明文件
简介
单独使用的模块,一般会同时提供一个单独的类型声明文件(declaration file),把本模块的外部接口的所有类型都写在这个文件里面,便于模块使用者了解接口,也便于编译器检查使用者的用法是否正确。
文件名一般为[模块名].d.ts
的形式,其中的d
表示 declaration(声明)。
类型声明文件的来源
类型声明文件主要有以下三种来源。
- TypeScript 编译器自动生成。
- TypeScript 内置类型文件。
- 外部模块的类型声明文件,需要自己安装。
1.自动生成
只要使用编译选项 declaration
,编译器就会在编译时自动生成单独的类型声明文件。
下面是在tsconfig.json
文件里面,打开这个选项。
{"compilerOptions": {"declaration": true}
}
2.内置声明文件
安装 TypeScript 语言时,会同时安装一些内置的类型声明文件,这些内置声明文件位于 TypeScript 语言安装目录的 lib 文件夹内。例如:
- lib.d.ts
- lib.dom.d.ts
- lib.es2015.d.ts
- lib.es2016.d.ts
- lib.es2017.d.ts
- lib.es2018.d.ts
- lib.es2019.d.ts
- lib.es2020.d.ts
- lib.es5.d.ts
- lib.es6.d.ts
TypeScript 编译器会自动根据编译目标 target 的值,加载对应的内置声明文件,所以不需要特别的配置。可以使用编译选项 lib,指定加载哪些内置声明文件。
{"compilerOptions": {"lib": ["dom", "es2021"]}
}
3.外部模块的类型声明文件
如果项目中使用了外部的某个第三方代码库,那么就需要这个库的类型声明文件。
分成三种情况。
(1)这个库自带了类型声明文件。
一般来说,如果这个库的源码包含了[vendor].d.ts
文件,那么就自带了类型声明文件。
(2)这个库没有自带,但是可以找到社区制作的类型声明文件。
第三方库如果没有提供类型声明文件,社区往往会提供。如:"@types/jquery"
。
TypeScript 会自动加载node_modules/@types
目录下的模块,但可以使用编译选项typeRoots
改变这种行为。
{"compilerOptions": {"typeRoots": ["./typings", "./vendor/types"]}
}
上面示例表示,TypeScript 不再去node_modules/@types
目录,而是去跟当前tsconfig.json
同级的typings
和vendor/types
子目录,加载类型模块了。
默认情况下,TypeScript 会自动加载typeRoots
目录里的所有模块,编译选项types
可以指定加载哪些模块。
{"compilerOptions": {"types": ["jquery"]}
}
(3)找不到类型声明文件,需要自己写。
有时实在没有第三方库的类型声明文件,又很难完整给出该库的类型描述,这时你可以告诉 TypeScript 相关对象的类型是any
。
declare var $: any;// 或者
declare type JQuery = any;
declare var $: JQuery;
模块发布
当前模块如果包含自己的类型声明文件,可以在 package.json 文件里面添加一个types
字段或typings
字段,指明类型声明文件的位置。
{"name": "awesome","author": "Vandelay Industries","version": "1.0.0","main": "./lib/main.js","types": "./lib/main.d.ts"
}
三斜杠命令
如果类型声明文件的内容非常多,可以拆分成多个文件,然后入口文件使用三斜杠命令,加载其他拆分后的文件。
举例来说,入口文件是main.d.ts
,里面的接口定义在interfaces.d.ts
,函数定义在functions.d.ts
。那么,main.d.ts
里面可以用三斜杠命令,加载后面两个文件。
/// <reference path="./interfaces.d.ts" />
/// <reference path="./functions.d.ts" />
注意,三斜杠命令只能用在文件的头部,如果用在其他地方,会被当作普通的注释。
三斜杠命令主要包含三个参数,代表三种不同的命令。
- path
- types
- lib
1./// <reference path="" />
告诉编译器在编译时需要包括的文件,常用来声明当前脚本依赖的类型文件。
/// <reference path="./lib.ts" />let count = add(1, 2);
上面示例表示,当前脚本依赖于./lib.ts
,里面是add()
的定义。编译当前脚本时,还会同时编译./lib.ts
。编译产物会有两个 JS 文件,一个当前脚本,另一个就是./lib.js
。
2./// <reference types="" />
types 参数用来告诉编译器当前脚本依赖某个 DefinitelyTyped 类型库,通常安装在node_modules/@types
目录。
/// <reference types="node" />
上面示例中,这个三斜杠命令表示编译时添加 Node.js 的类型库,实际添加的脚本是 node_modules 目录里面的@types/node/index.d.ts
。
/// <reference lib="" />
允许脚本文件显式包含内置 lib 库,等同于在tsconfig.json
文件里面使用 lib 属性指定 lib
库。
13.TypeScript 类型运算符
keyof 运算符
keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。
type MyObj = {foo: number;bar: string;
};type Keys = keyof MyObj; // 'foo'|'bar'
由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol
。
// string | number | symbol
type KeyT = keyof any;
对于没有自定义键名的类型使用 keyof 运算符,返回never
类型,表示不可能有这样类型的键名。
type KeyT = keyof object; // never
keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型,可以像下面这样写。
type MyObj = {foo: number;bar: string;
};type Keys = keyof MyObj;type Values = MyObj[Keys]; // number|string
keyof 运算符往往用于精确表达对象的属性类型。
function prop<Obj, K extends keyof Obj>(obj: Obj, key: K): Obj[K] {return obj[key];
}
上面示例中,K extends keyof Obj
表示K
是Obj
的一个属性名,传入其他字符串会报错。返回值类型Obj[K]
就表示K
这个属性值的类型。
keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。
type NewProps<Obj> = {[Prop in keyof Obj]: boolean;
};// 用法
type MyObj = { foo: number };// 等于 { foo: boolean; }
type NewObj = NewProps<MyObj>;
下面的例子是让可选属性变成必有的属性。
type Concrete<Obj> = {[Prop in keyof Obj]-?: Obj[Prop];
};// 用法
type MyObj = {foo?: number;
};// 等于 { foo: number; }
type NewObj = Concrete<MyObj>;
上面示例中,[Prop in keyof Obj]
后面的-?
表示去除可选属性设置。对应地,还有+?
的写法,表示添加可选属性设置。
in 运算符
JavaScript 语言中,in
运算符用来确定对象是否包含某个属性名。
const obj = { a: 123 };if ('a' in obj) console.log('found a');
TypeScript 语言的类型运算中,in
运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。
type U = 'a' | 'b' | 'c';type Foo = {[Prop in U]: number;
};
// 等同于
type Foo = {a: number;b: number;c: number;
};
方括号运算符
方括号运算符([]
)用于取出对象的键值类型,比如T[K]
会返回对象T
的属性K
的类型。
type Person = {age: number;name: string;alive: boolean;
};// Age 的类型是 number
type Age = Person['age'];
方括号的参数如果是联合类型,那么返回的也是联合类型。
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];
extends…?: 条件运算符
条件运算符extends...?:
可以根据当前类型是否符合某种条件,返回不同的类型。
// 类型T是否可以赋值给类型U,即T是否为U的子类型
T extends U ? X : Y// true
type T = 1 extends number ? true : false;
infer 关键字
infer
关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。
is 运算符
函数返回布尔值的时候,可以使用is
运算符,限定返回值与参数之间的关系。
function isFish(pet: Fish | Bird): pet is Fish {return (pet as Fish).swim !== undefined;
}
上面示例中,函数isFish()
的返回值类型为pet is Fish
,表示如果参数pet
类型为Fish
,则返回true
,否则返回false
。
satisfies 运算符
satisfies
运算符用来检测某个值是否符合指定类型。
14.类型映射
简介
映射(mapping)指的是,将一种类型按照映射规则,转换成另一种类型,通常用于对象类型。
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;
};
具体的计算规则如下:
prop
:属性名变量,名字可以随便起。in
:运算符,用来取出右侧的联合类型的每一个成员。keyof A
:返回类型 A 的每一个属性名,组成一个联合类型。
TypeScript 内置的工具类型1Readonly<T>
可以将所有属性改为只读属性,实现也是通过映射。
// 将 T 的所有属性改为只读属性
type Readonly<T> = {readonly [P in keyof T]: T[P];
};
映射修饰符
映射会原样复制原始对象的可选属性和只读属性。
type A = {a?: string;readonly b: number;
};type B = {[Prop in keyof A]: A[Prop];
};// 等同于
type B = {a?: string;readonly b: number;
};
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];
};
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;
};
属性过滤
键名重映射还可以过滤掉某些属性。下面的例子是只保留字符串属性。
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
,即这个属性名不存在。
15.类型工具
Awaited<Type>
,用来取出 Promise 的返回值类型ConstructorParameters<Type>
,提取构造方法Type的参数类型,组成一个元组类型返回Exclude<UnionType,ExcludedMembers>
,用来从联合类型UnionType里面,删除某些类型ExcludedMembers,组成一个新的类型返回Extract<UnionType, Union>
,用来从联合类型UnionType之中,提取指定类型Union,组成一个新类型返回InstanceType<Type>
,提取构造函数的返回值的类型(即实例类型)NonNullable<Type>
,用来从联合类型Type删除null类型和undefined类型,组成一个新类型返回Omit<Type, Keys>
,用来从对象类型Type中,删除指定的属性Keys,组成一个新的对象类型返回OmitThisParameter<Type>
,从函数类型中移除 this 参数Parameters<Type>
,从函数类型Type里面提取参数类型,组成一个元组返回Partial<Type>
,返回一个新类型,将参数类型Type的所有属性变为可选属性Pick<Type, Keys>
,返回一个新的对象类型,第一个参数Type是一个对象类型,第二个参数Keys是Type里面被选定的键名Readonly<Type>
,返回一个新类型,将参数类型Type的所有属性变为只读属性Record<Keys, Type>
,返回一个对象类型,参数Keys用作键名,参数Type用作键值类型Required<Type>
,返回一个新类型,将参数类型Type的所有属性变为必选属性ReadonlyArray<Type>
,用来生成一个只读数组类型,类型参数Type表示数组成员的类型ReturnType<Type>
,提取函数类型Type的返回值类型,作为一个新类型返回ThisParameterType<Type>
,提取函数类型中this参数的类型ThisType<Type>
,不返回类型,只用来跟其他类型组成交叉类型,用来提示 TypeScript 其他类型里面的this的类型- 字符串类型工具
Uppercase<StringType>
,将字符串类型的每个字符转为大写Lowercase<StringType>
,将字符串的每个字符转为小写Capitalize<StringType>
,将字符串的第一个字符转为大写Uncapitalize<StringType>
,将字符串的第一个字符转为小写
16.注释指令
// @ts-nocheck
// @ts-nocheck
告诉编译器不对当前脚本进行类型检查,可以用于 TypeScript 脚本,也可以用于 JavaScript 脚本。
// @ts-nocheckconst element = document.getElementById(123);
// @ts-check
如果一个 JavaScript 脚本顶部添加了// @ts-check
,那么编译器将对该脚本进行类型检查,不论是否启用了checkJs
编译选项。
// @ts-check
let isChecked = true;console.log(isChceked); // 报错, 拼写错误
// @ts-ignore
// @ts-ignore
告诉编译器不对下一行代码进行类型检查,可以用于 TypeScript 脚本,也可以用于 JavaScript 脚本。
let x: number;x = 0;// @ts-ignore
x = false; // 不报错
// @ts-expect-error
// @ts-expect-error
主要用在测试用例,当下一行有类型错误时,它会压制 TypeScript 的报错信息(即不显示报错信息),把错误留给代码自己处理。
JSDoc
TypeScript 直接处理 JS 文件时,如果无法推断出类型,会使用 JS 脚本里面的 JSDoc 注释。
JSDoc 基本要求:
- JSDoc 注释必须以
/**
开始,其中星号(*
)的数量必须为两个。若使用其他形式的多行注释,则 JSDoc 会忽略该条注释。 - JSDoc 注释必须与它描述的代码处于相邻的位置,并且注释在上,代码在下。
下面是 JSDoc 的一个简单例子。
/*** @param {string} somebody*/
function sayHello(somebody) {console.log('Hello ' + somebody);
}
TypeScript 编译器支持大部分的 JSDoc 声明,举例如下:
@typedef
,创建自定义类型,等同于 TypeScript 里面的类型别名。
/*** @typedef {(number | string)} NumberLike*/// 等同于
type NumberLike = string | number;
@type
,定义变量的类型。
/*** @type {string}*/
let a;// @type定义了变量a的类型为string// 在@type命令中允许使用 TypeScript 类型及其语法。
/**@type {true | false} */
let a;/** @type {number[]} */
let b;/** @type {Array<number>} */
let c;
@param
,用于定义函数参数的类型。
/*** @param {string} x*/
function foo(x) {}// 如果是可选参数,需要将参数名放在方括号[]里面
/*** @param {string} [x]*/
function foo(x) {}// 可以指定参数默认值
/*** @param {string} [x="bar"]*/
function foo(x) {}
@return,@returns
,两个命令作用相同,指定函数返回值的类型。
/*** @return {boolean}*/
function foo() {return true;
}
@extends 和类型修饰符
,@extends 命令用于定义继承的基类。
/*** @extends {Base}*/
class Derived extends Base {}// @public、@protected、@private分别指定类的公开成员、保护成员和私有成员。
// @readonly指定只读成员。
class Base {/*** @public* @readonly*/x = 0;/*** @protected*/y = 0;
}
参考文档
- TypeScript 英文文档
- TypeScript 翻译的中文文档
- 网道-阮一峰-TypeScript 教程