交叉类型
交叉类型是将多个类型合并为⼀个类型。 这让我们可以把现有的多种类型叠加到⼀起成为⼀种类型, 它包含了所需的所有类型的特性。
在 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>;
}
type和interface的区别:
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 ,然后声明⼀个接⼝ Dog , Dog 的属性 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 ⾮常强⼤,由于它的存在可以做出⾮常多的骚操作. tuple转union,⽐如[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>
关键字
像 keyof 、 typeof 这种常⽤关键字我们已经了解过了,当然还有很常⽤的 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 的编译流程也可以粗略得分为三步:
- 解析
- 转换
- 生成
结合上部分的编译器各个组成部分,流程如下图:

验证成果
一道题 题目地址
