Skip to main content

typescript类型编程

· 31 min read
石晓波

交叉类型

交叉类型是将多个类型合并为⼀个类型。 这让我们可以把现有的多种类型叠加到⼀起成为⼀种类型, 它包含了所需的所有类型的特性。
在 JavaScript 中,混⼊是⼀种⾮常常⻅的模式,在这种模式中,你可以从两个对象中创建⼀个新对 象,新对象会拥有着两个对象所有的功能。

function mixin<T extends object, U extends object>(first: T, second: U): T & U {
const result = <T & U>{};
for (let id in first) {
(<T>result)[id] = first[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<U>result)[id] = second[id];
}
}

return result;
}

const x = mixin({ a: 'hello' }, { b: 42 });

// 现在 x 拥有了 a 属性与 b 属性
console.log(x.a);
console.log(x.b);

联合类型

JavaScript 中,希望属性为多种类型之⼀,如字符串或者数组。 这就是联合类型所能派上⽤场的地⽅(它使⽤ | 作为标记,如 string | number)。

function formatCommandline(command: string[] | string) {
let line = '';
if (typeof command === 'string') {
line = command.trim();
} else {
line = command.join(' ').trim();
}
}

类型别名

type some = boolean | string
const b: some = true // ok
const c: some = 'hello' // ok
const d: some = 123 // 不能将类型“123”分配给类型“some”
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}

typeinterface的区别:
interface 只能⽤于定义对象类型,⽽ type 的声明⽅式除了对象之外还可以定义交叉、联合、原始类 型等,类型声明的⽅式适⽤范围显然更加⼴泛。
但是interface也有其特定的⽤处:

  • interface ⽅式可以实现接⼝的 extends 和 implements
  • interface 可以实现接⼝合并声明
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

接⼝创建了⼀个新的名字,可以在其它任何地⽅使⽤,类型别名并不创建新名字,⽐如,错误信息就不会使⽤别名。

可辨识联合类型

先假设⼀个场景,现在⼜两个功能,⼀个是创建⽤⼾即 create ,⼀个是删除⽤⼾即 delete . 我们先定义⼀下这个接⼝,由于创建⽤⼾不需要id,是系统随机⽣成的,⽽删除⽤⼾是必须⽤到 id 的,那么 代码如下:

interface Info {
username: string
}
interface UserAction {
id?: number
action: 'create' | 'delete'
info: Info
}

上⾯的接⼝是不是有什么问题? 是的,当我们创建⽤⼾时是不需要 id 的,但是根据上⾯接⼝产⽣的情况,以下代码是合法的:

const action:UserAction = {
action:'create',
id: 111,
info: {
username: 'xiaomuzhu'
}

}

但是我们明明不需要 id 这个字段,因此我们得⽤另外的⽅法,这就⽤到了上⾯提到的「字⾯量类型」了:

interface Info {
username: string
}

type UserAction = {
id: number
action: 'delete'
info: Info
} |
{
action: 'create'
info: Info
}
const UserReducer = (userAction: UserAction) => {
switch (userAction.action) {
case 'delete':
console.log(userAction.id);

break;
default:

break;
}
}
// 我们上面提到了 userAction.action 就是辨识的关键, 被称为可辨识的标签, 我们发现上面这种模式要想实现必须要三个要素:

// 具有普通的单例类型属性—可辨识的特征, 上文中就是 delete 与 create 两个有唯一性的字符串字面量
// 一个类型别名包含联合类型
// 类型守卫的特性, 比如我们必须用 if switch 来判断 userAction.action 是属于哪个类型作用域即 delete 与 create

interface Person {
name: string;
age: number;
}
const person = {} as Person;
person.name = 'xiaomuzhu';
person.age = 20;

字面量类型

字⾯量(Literal Type)主要分为 真值字⾯量类型(boolean literal types),数字字⾯量类型 (numeric literal types),枚举字⾯量类型(enum literal types),⼤整数字⾯量类型(bigInt literal types)和字符串字⾯量类型(string literal types)。

const a: 2333 = 2333 // ok
const ab : 0b10 = 2 // ok
const ao : 0o114 = 0b1001100 // ok
const ax : 0x514 = 0x514 // ok
const b : 0x1919n = 6425n // ok
const c : 'xiaomuzhu' = 'xiaomuzhu' // ok
const d : false = false // ok
const g: 'github' = 'pronhub' // 不能将类型“"pronhub"”分配给类型“"github"”

当字⾯量类型与联合类型结合的时候,⽤处就显现出来了,它可以模拟⼀个类似于枚举的效果:

type Direction = 'North' | 'East' | 'South' | 'West';
function move(distance: number, direction: Direction) {
//...
}

类型字面量

类型字⾯量(Type Literal)不同于字⾯量类型(Literal Type),它跟 JavaScript 中的对象字⾯量的语法很 相似:

type Foo = {
baz: [
number,
'xiaomuzhu'
];
toString(): string;
readonly [Symbol.iterator]: 'github';
0x1: 'foo';
"bar": 12n;
};

类型断言

初学者经常会遇到的⼀类问题:

 const person = {};
person.name = 'xiaomuzhu'; // Error: 'name' 属性不存在于 ‘{}’
person.age = 20; // Error: 'age' 属性不存在于 ‘{}’

这个时候该怎么办?由于类型推断,这个时候 person 的类型就是 {} ,根本不存在后添加的那些属 性,虽然这个写法在js中完全没问题,但是开发者知道这个 person 实际是有属性的,只是⼀开始没 有声明⽽已,但是 typescript 不知道啊,所以就需要类型断⾔了:

interface Person {
name: string;
age: number;
}
const person = {} as Person;
person.name = 'xiaomuzhu';
erson.age = 20;

双重断言

interface Person {
name: string;
age: number;
}
const person = 'xiaomuzhu' as Person; // Error
const person = 'xiaomuzhu' as any as Person; // ok

类型守卫

intanceof、in

class Person {
name = 'xiaomuzhu';
age = 20;
}

class Animal {
name = 'petty';
color = 'pink';
}

function getSometing(arg: Person | Animal) {
if (arg instanceof Person) {
console.log(arg.color); // Error
console.log(arg.age); // ok
}
if (arg instanceof Animal) {
console.log(arg.color); // ok
console.log(arg.age); // Error
}
}

function getSometing(arg: Person | Animal) {
if ('age' in arg) {
console.log(arg.color); // Error
console.log(arg.age); // ok
}
if ('color' in arg) {
console.log(arg.age); // Error
console.log(arg.color); // ok
}
}

类型兼容性

结构类型

TypeScript ⾥的类型兼容性是基于「结构类型」的,结构类型是⼀种只使⽤其成员来描述类型的⽅ 式,其基本规则是,如果 x 要兼容 y,那么 y ⾄少具有与 x 相同的属性。x=y 我做⼀个简单的实验,构建⼀个类 Person ,然后声明⼀个接⼝ DogDog 的属性 Person 都拥有,⽽且还多了其他属性,这种情况下 Dog 兼容了 Person

 class Person {
constructor(public weight: number, public name: string, public born: string) {
}
}

interface Dog {
name: string
weight: number
}
let x: Dog
x = new Person(120, 'cxk', '1996-12-12') // OK

但反过来就不行,总结小的兼容大的

函数的兼容性

函数类型的兼容性判断,要查看 x 是否能赋值给 y,⾸先看它们的参数列表。 x 的每个参数必须能在 y ⾥找到对应类型的参数,注意的是参数的名字相同与否⽆所谓,只看它们的类 型。

let q = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = q; // OK
q = y; // Error 不能将类型“(b: number, s: string) => number”分配给类型“(a: number) => number”。

let foo = (x: number, y: number) => { };
let bar = (x?: number, y?: number) => { };
let bas = (...args: number[]) => { };

// foo = bar = bas;
// bas = bar = foo;
//当我们把 strictNullChecks 设置为 false 时上述代码是兼容的。

let foo2 = (x: number, y: number) => { };
let bar2 = (x?: number) => { };

// foo2 = bar // ok
// bar2 = foo2 //报错

参数多的兼容参数少的,也就是参数少的可以赋值给参数多的

类的类型兼容性

//仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查:
class Animal {
feet: number;
constructor(name: string, numFeet: number) {
this.feet = numFeet
}
}

class Size {
feet: number;
constructor(meters: number) {
this.feet = meters
}
}

let a: Animal = new Animal('a', 2);
let s: Size = new Size(1);

a = s; // OK
s = a; // OK

泛型的类型兼容性

泛型本⾝就是不确定的类型,它的表现根据是否被成员使⽤⽽不同.

interface Person<T> {

}
let x : Person<string>
let y : Person<number>
x = y // ok
y = x // ok

由于没有被成员使⽤泛型,所以这⾥是没问题的。
接着看如下案例:

interface Person<T> {
name: T
}
let x : Person<string>
let y : Person<number>
x = y // 不能将类型“Person<number>”分配给类型“Person<string>”。
y = x // 不能将类型“Person<string>”分配给类型“Person<number>”。

is关键字

function isString(test: any): test is string {
return typeof test === 'string';
}

function example(foo: number | string) {
if (isString(foo)) {
console.log('it is a string' + foo);
console.log(foo.length); // string function
} else {
console.log(foo)
}
}
example('hello world');

可调⽤类型注解

//我们已经可以用静态类型注解我们的函数、参数等等,但是假设我们有一个接口,我们如何操作才能让它被注解为可执行的:
interface ToString {
(): string
new(): string
}
declare const sometingToString: ToString;
sometingToString() // This expression is not callable. Type 'ToString' has no call signatures.ts(2349)
new sometingToString()

高级类型之索引类型、映射类型、条件类型

索引类型

先看⼀个场景,现在我们需要⼀个 pick函数,这个函数可以从对象上取出指定的属性,类似于 lodash.pick ⽅法。
javascript:

function pick(o, names) {
return names.map(n => o[n]);
}
const user = {
username: 'Jessica Lee',
id: 460000201904141743,
token: '460000201904141743',
avatar: 'http://dummyimage.com/200x200',
role: 'vip'
}
const res = pick(user, ['id'])
console.log(res) // [ '460000201904141743' ]

typescript简陋版:

interface Obj {
[key: string]: any
}
function pick(o: Obj, names: string[]) {
return names.map(n => o[n]);
}

高级框架版:

type key = keyof T === 'username' | 'id' ...

function pick<T, K extends keyof T>(o:T,names:K[]):T[K][]{
return names.map(n => o[n])
}
const res = pick(user, ['token', 'id', ])

映射类型

有⼀个User接⼝,现在有⼀个需求是把User接⼝中的成员全部变成可选的,我们应该怎么做?难 道要重新⼀个个 : 前⾯加上 ? ,有没有更便捷的⽅法?
这个时候映射类型就派上⽤场了,映射类型的语法是 [K in Keys] :

  • K:类型变量,依次绑定到每个属性上,对应每个属性名的类型
  • Keys:字符串字⾯量构成的联合类型,表⽰⼀组属性名(的类型)
type partial<T> = { [K in keyof T]?: T[K] }
interface User3 {
username: string
id: number
token: string
avatar: string
role: string
}
type Keyof = keyof User3
type partial<T> = { [K in keyof T]?: T[K] }
type partialUser = partial<User3>
type readonlyUser = Readonly<User3>
//Required Pick Record Exclude Extract NonNullable

declare function f<T extends boolean>(x: T): T extends true ? string : number;

const x3 = f(Math.random() < 0.5)
const y3 = f(false)
const z = f(true)

条件类型

条件类型够表⽰⾮统⼀的类型,以⼀个条件表达式进⾏类型关系检测,从⽽在两种类型中选择其⼀:

T extends U ? X : Y

上⾯的代码可以理解为: 若 T 能够赋值给 U ,那么类型是 X,否则为 Y ,有点类似于JavaScript中的 三元条件运算符.

⽐如声明⼀个函数 f ,它的参数接收⼀个布尔类型,当布尔类型为 true 时返回 string 类型,否 则返回 number 类型:

declare function f<T extends boolean>(x: T): T extends true ? string : number;

const x = f(Math.random() < 0.5)
const y = f(false)
const z = f(true)

条件类型就是这样,只有类型系统中给出充⾜的条件之后,它才会根据条件推断出类型结果.

条件类型与联合类型

条件类型有⼀个特性,就是「分布式有条件类型」,但是分布式有条件类型是有前提的,条件类型⾥待检 查的类型必须是 naked type parameter . naked type parameter 指的是裸类型参数,怎么理解?这个「裸」是指类型参数没有被包装在其 他类型⾥,⽐如没有被数组、元组、函数、Promise等等包裹.

// 裸类型参数,没有被任何其他类型包裹即T
type NakedUsage<T> = T extends boolean ? "YES" : "NO"

// 类型参数被包裹的在元组内即[T]
type WrappedUsage<T> = [T] extends [boolean] ? "YES" : "NO";

这⼀部分⽐较难以理解,可以把「分布式有条件类型」粗略得理解为类型版的 map()⽅法 ,然后我 们再看⼀些实⽤案例加深理解.

/ 裸类型参数,没有被任何其他类型包裹即T
type NakedUsage<T> = T extends boolean ? "YES" : "NO"
// 类型参数被包裹的在元组内即[T]
type WrappedUsage<T> = [T] extends [boolean] ? "YES" : "NO";

type Distributed = NakedUsage<number | boolean> // = NakedUsage<number> | NakedUsage<boolean> = "NO" | "YES"
type NotDistributed = WrappedUsage<number | boolean> // "NO"



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

type R = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;
type Temp2 = never| "b"| never| "d"
// "b" | "d"|

type Filter<T, U> = T extends U ? T : never;
type R1 = Filter<string | number | (() => void), Function>;

// 剔除 null和undefined
type NonNullable2<T> = Diff<T, null | undefined>;

type R2 = NonNullable2<string | number | undefined>; // string | number

条件类型与映射类型

在⼀些有要求TS基础的公司,设计⼯具类型是⼀个⽐较⼤的考点.

//我有一个interface Part, 现在需要编写一个工具类型将interface中函数类型的名称取出来, 在这个题目示例中, 应该取出的是:
interface Part {
id: number;
name: string;
subparts: Part[];
updatePart(newName: string): void;
}
interface Part2 {
id2: number;
name2: string;
subparts3: Part[];
updatePart111(newName: string): void;
updatePart222(newName: string): void;
}

//这种问题我们应该换个思路,比如我们把interface看成js中的对象字面量,用js的思维你会如何取出?
//这个时候问题就简单了, 遍历整个对象, 找出value是函数的部分取出key即可.
//在TypeScript的类型编程中也是类似的道理, 我们要遍历interface, 取出类型为Function的部分找出key即可:
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]
type R3 = FunctionPropertyNames<Part>;
type R89 = FunctionPropertyNames<Part2>;
// 1假设我们把Part代入泛型T, [K in keyof T]相当于遍历整个interface
// 2这时K相当于interface的key, T[K]相当于interface的value
// 3接下来, 用条件类型验证value的类型, 如果是Function那么将value作为新interface的key保留下来, 否则为never
// 4到这里我们得到了遍历修改后的新interface即:
// type R7 = {
// id: never;
// name: never;
// subparts: never;
// updatePart: "updatePart";
// }[keyof Part]
// type T = keyof Part
//但是我们的的要求是取出老interface Part的key, 这个时候再次用[keyof T]作为key依次取出新interface的value,
//但是由于id name和subparts的value为never就不会返回任何类型了, 所以只返回了'updatePart'.

强⼤的infer关键字

infer 是⼯具类型和底层库中⾮常常⽤的关键字,表⽰在 extends 条件语句中待推断的类型变量,相对 ⽽⾔也⽐较难理解,我们不妨从⼀个 typescript ⾯试题开始: 之前学过 ReturnType ⽤于获取函数的返回类型,那么如何设计⼀个 ReturnType ?
infer ⾮常强⼤,由于它的存在可以做出⾮常多的骚操作. tupleunion,⽐如[string, number] -> string | number:

type ElementOf<T> = T extends Array<infer E> ? E : never;
type TTuple = [string, number];
type ToUnion = ElementOf<ATuple>; // string | number
class TestClass {
constructor(public name: string, public age: number) { }
}
type ConstructorParameters5<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any
? P
: never;
type R4 = ConstructorParameters5<typeof TestClass> // [string, number]

//new (...args: any[]) => any指构造函数, 因为构造函数是可以被实例化的.
//infer P代表待推断的构造函数参数, 如果接受的类型T是一个构造函数, 那么返回构造函数的参数类型P, 否则什么也不返回, 即never类型

常用的工具类型解读

⽤ JavaScript 编写中⼤型程序是离不开 lodash 这种⼯具集的,⽽⽤ TypeScript 编程同样离不开类型 ⼯具的帮助,类型⼯具就是类型版的 lodash.
如下会介绍⼀些类型⼯具的设计与实现,如果项⽬不是⾮常简单的 demo 级项⽬,那么在开发过程中⼀定会⽤到它们。
起初,TypeScript 没有这么多⼯具类型,很多都是社区创造出来的,然后 TypeScript 陆续将⼀些常 ⽤的⼯具类型纳⼊了官⽅基准库内。
⽐如 ReturnType 、 Partial 、 ConstructorParameters 、 Pick 都是官⽅的内置⼯具类型. 其实上述的⼯具类型都可以被我们开发者⾃⼰模拟出来,本节学习⼀下如何设计⼯具类型.

泛型

可以把⼯具类型类⽐ js 中的⼯具函数,因此必须有输⼊和输出,⽽在TS的类型系统中能担当 类型⼊⼝的只有泛型.
⽐如 Partial ,它的作⽤是将属性全部变为可选.

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

这个类型⼯具中,需要将类型通过泛型 T 传⼊才能对类型进⾏处理并返回新类型,可以说,⼀切类型 ⼯具的基础就是泛型.

类型递归

interface Company {
id: number
name: string
}

interface Person {
id: number
name: string
adress: string
company: Company
}

type R0 = Partial<Person>

type DeepPartial<T> = {
[U in keyof T]?: T[U] extends object
? DeepPartial<T[U]>
: T[U]
};

type R9 = DeepPartial<Person>

关键字

keyoftypeof 这种常⽤关键字我们已经了解过了,当然还有很常⽤的 Type inference infer 关键字的使⽤,还有之前的 Conditional Type 条件类型,现在主要谈⼀下另外⼀些常⽤关键字. + - 这两个关键字⽤于映射类型中给属性添加修饰符,⽐如 -? 就代表将可选属性变为必选, - readonly 代表将只读属性变为⾮只读. ⽐如TS就内置了⼀个类型⼯具 Required<T>,它的作⽤是将传⼊的属性变为必选项:

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

常⻅⼯具类型

Omit

Omit 这个⼯具类型在开发过程中⾮常常⻅,以⾄于官⽅在3.5版本正式加⼊了 Omit 类型.
要了解之前先看⼀下另⼀个内置类型⼯具的实现 Exclude<T> :

type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<1 | 2, 1 | 3> // -> 2

Exclude 的作⽤是从 T 中排除出可分配给 U 的元素. 这⾥的可分配即 assignable ,指可分配的, T extends U 指T是否可分配给U Omit = Exclude + Pick

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
type Foo = Omit<{name: string, age: number}, 'name'> // -> { age: number }

Omit<T, K> 的作⽤是忽略 T 中的某些属性.

Merge

Merge<O1, O2> 的作⽤是将两个对象的属性合并:

type O1 = {
name: string
id: number
}
type O2 = {
id: number
from: string
}
type R2 = Merge<O1, O2>

这个类型⼯具也⾮常常⽤,他主要有两个部分组成: Merge<O1, O2> = Compute<A> + Omit<U, T>
Compute 的作⽤是将交叉类型合并.即:

type Compute<A extends any> =
A extends Function
? A
: { [K in keyof A]: A[K] }
type R1 = Compute<{x: 'x'} & {y: 'y'}>

Merge的最终实现如下:

type Merge<O1 extends object, O2 extends object> =  
Compute<O1 & Omit<O2, keyof O1>>

Intersection

Intersection<T, U> 的作⽤是取 T 的属性,此属性同样也存在与 U.

type Props = { name: string; age: number; visible: boolean };
type DefaultProps = { age: number };

// Expect: { age: number; }
type DuplicatedProps = Intersection<Props, DefaultProps>;

实现

type Intersection<T extends object, U extends object> = Pick<
T,
Extract<keyof T, keyof U> & Extract<keyof U, keyof T>
>;

Overwrite

Overwrite<T, U>顾名思义,是⽤ U 的属性覆盖 T 的相同属性.

type Props = { name: string; age: number; visible: boolean };
type NewProps = { age: string; other: string };

// Expect: { name: string; age: string; visible: boolean; }
type ReplacedProps = Overwrite<Props, NewProps>

即:

type Overwrite<
T extends object,
U extends object,
I = Diff<T, U> & Intersection<U, T>
> = Pick<I, keyof I>;

Mutable

T 的所有属性的 readonly 移除

type Mutable<T> = {
-readonly [P in keyof T]: T[P]
}

Record

Record 允许从 Union 类型中创建新类型,Union 类型中的值⽤作新类型的属性。

type Car = 'Audi' | 'BMW' | 'MercedesBenz'
type CarList = Record<Car, {age: number}>

const cars: CarList = {
Audi: { age: 119 },
BMW: { age: 113 },
MercedesBenz: { age: 133 },
}

在实战项⽬中尽量多⽤ Record,它会帮助规避很多错误,在 vue 或者 react 中有很多场景选择 Record 是更优解。

巧⽤类型约束

在 .tsx ⽂件⾥,泛型可能会被当做 jsx 标签

const toArray = <T>(element: T) => [element]; // Error in .tsx file.

加 extends 可破

const toArray = <T extends {}>(element: T) => [element]; // No errors.

模块与命名空间

模块系统

TypeScript 与 ECMAScript 2015 ⼀样,任何包含顶级 import 或者 export 的⽂件都被当成⼀个 模块。
相反地,如果⼀个⽂件不带有顶级的 import 或者 export 声明,那么它的内容被视为全局可⻅的。

模块语法

可以⽤ export 关键字导出变量或者类型,⽐如:

// export.ts
export const a = 1
export type Person = {
name: String
}

如果想⼀次性导出,那么你可以:

const a = 1
type Person = {
name: String
}
export { a, Person }
import { a, Person } from './export';

同样的也可以重命名导⼊的模块:

import { Person as P } from './export';

如果不想⼀个个导⼊,想把模块整体导⼊,可以这样:

import * as P from './export';

甚⾄可以导⼊后导出模块:

export { Person as P } from './export';

当然,除了上⾯的⽅法之外还有默认的导⼊导出:

 export default (a = 1)export default () => 'function'

命名空间

命名空间⼀个最明确的⽬的就是解决重名问题。
TypeScript 中命名空间使⽤ namespace 来定义,语法格式如下:

namespace SomeNameSpaceName {
export interface ISomeInterfaceName { }
export class SomeClassName { }
}

以上定义了⼀个命名空间 SomeNameSpaceName,如果需要在外部可以调⽤ SomeNameSpaceName 中的类和接⼝,则需要在类和接⼝添加 export 关键字. 其实⼀个 命名空间 本质上⼀个 对象 ,它的作⽤是将⼀系列相关的全局变量组织到⼀个对象的属性 你在⼿动构建⼀个命名空间,但是在 ts 中, namespace 提供了⼀颗语法糖。上述可⽤语法糖改写 成:

namespace Letter {
export let a = 1;
export let b = 2;
export let c = 3;
// ...
export let z = 26;

编辑成 js

var Letter;
(function (Letter) {
Letter.a = 1;
Letter.b = 2;
Letter.c = 3;
// ...
Letter.z = 26;
})(Letter || (Letter = {}));

命名空间的⽤处

命名空间在现代TS开发中的重要性并不⾼,主要原因是ES6引⼊了模块系统,⽂件即模块的⽅式使得开发 者能更好的得组织代码,但是命名空间并⾮⼀⽆是处,通常在⼀些⾮ TypeScript 原⽣代码的 .d.ts ⽂ 件中使⽤,主要是由于 ES Module 过于静态,对 JavaScript 代码结构的表达能⼒有限。 因此在正常的TS项⽬开发过程中并不建议⽤命名空间。

使⽤第三⽅ d.ts

Github 上有⼀个库 DefinitelyTyped 它定义了市⾯上主流的JavaScript 库的 d.ts ,⽽且我们可以很⽅ 便地⽤ npm 引⼊这些 d.ts。

编写 d.ts ⽂件

关键字 declare 表⽰声明的意思,我们可以⽤它来做出各种声明:

  • declare var 声明全局变量
  • declare function 声明全局⽅法
  • declare class 声明全局类
  • declare enum 声明全局枚举类型
  • declare namespace 声明(含有⼦属性的)全局对象
  • interface 和 type 声明全局类型

TypeScript 的编译原理

编译器的组成

TypeScript有⾃⼰的编译器,这个编译器主要有以下部分组成:

  • Scanner 扫描器
  • Parser 解析器
  • Binder 绑定器
  • Emitter 发射器
  • Checker 检查器

编译器的处理

扫描器通过扫描源代码⽣成token流:

SourceCode(源码)+ 扫描器 --> Token 流

解析器将token流解析为抽象语法树(AST):

Token 流 + 解析器 --> AST(抽象语法树)

绑定器将AST中的声明节点与相同实体的其他声明相连形成符号(Symbols),符号是语义系统的主要构 造块:

AST + 绑定器 --> Symbols(符号)

检查器通过符号和AST来验证源代码语义:

AST + 符号 + 检查器 --> 类型验证

最后我们通过发射器⽣成JavaScript代码:

AST + 检查器 + 发射器 --> JavaScript 代码

编译器处理流程

TypeScript 的编译流程也可以粗略得分为三步:

  • 解析
  • 转换
  • 生成

结合上部分的编译器各个组成部分,流程如下图:

Locale Dropdown

验证成果

一道题 题目地址