
前言
TypeScript
是一种由微软开发的开源编程语言,它是 JavaScript
的超集,为 JavaScript
提供了静态类型检查和面向对象编程的能力。与传统的 JavaScript
相比,TypeScript
强调类型安全和代码可维护性,使开发者能够更轻松地编写可靠的大型应用程序。
TypeScript
的优势体现在以下几个方面:
- 静态类型检查 :
TypeScript
引入了静态类型系统,使开发者能够在编码阶段捕获潜在的类型错误,提高代码质量和可靠性。类型检查还能提供代码补全、重构和文档化的功能,提高开发效率。 - 面向对象编程 :
TypeScript
支持类、接口、继承等面向对象编程的特性,使代码更具结构性和可读性。通过类的定义和继承,开发者可以更好地组织和重用代码。 - JavaScript 生态系统的无缝整合 :
TypeScript
是JavaScript
的超集,现有的JavaScript
代码可以无缝迁移到TypeScript
中,并且TypeScript
可以直接使用JavaScript
的库和框架,扩展了开发者的选择和灵活性。 - 强大的工具支持 :
TypeScript
提供了强大的编译器和开发工具支持,包括代码补全、语法高亮、重构等功能。此外,许多流行的编辑器和集成开发环境(IDE)都提供了对TypeScript
的良好支持,如Visual Studio Code
、WebStorm
等。

TypeScript开发环境
安装
全局安装Typescript
:
bash
npm install -g typescript
// 查看版本
tsc -v
初始化配置文件,创建 TypeScript
的项目配置文件: tsconfig.json
。通过--locale zh-CN
命令创建出来的配置都是中文。
bash
tsc --init --locale zh-CN
编译执行
编译ts
文件
bash
tsc index.ts
编译完成后会在目录下生成对应文件名的.js
文件,里面就是编译后的代码,然后再执行这个.js
文件就可以看到结果。
这样做太麻烦了,可以安装ts-node
快速执行.ts
文件
bash
npm i ts-node -g
以后执行编译就可以通过这个命令
bash
ts-node index.ts
VsCode插件
TypeScript Importer插件可以自动搜索文件中的TypeScript
定义

Error Lens 插件可以将语法错误,直接显示在当前代码后面,例如

Playground TypeScript
在线工具,还提供了ts
配置文件选择
VsCode
配置TypeScript
类型提示,可以自动获取数据类型并在代码里面提示你。
打开首选项设置
, 在搜索框输入TypeScript Inlay Hints
,然后根据自己的需求打开提示。注意要勾选TypeScript
开头的配置,如果勾选JavaScript
就表示js
文件也生效,建议只打开TypeScript
文件就行。

在ts
文件中效果就是这样

当ts报错时,提示的报错信息为英文,通过设置可以将报错信息改为中文,更方便排查。
打开设置,在搜索栏输入typescript locale
,将选项选择为zh-CN

现在报错就都是中文

在tsc
编译阶段,也可以通过tsc demo.ts --locale zh-CN
来查看中文错误

TypeScrtip基础
数据类型
原始数据类型
字符串
使用string
定义类型
ts
const str:string = '泰裤辣'
还可以使用模版字符串
ts
const name: string = '法外狂徒张三'
const age: number = 18
let sentence: string = `${name}今年${age}了` // "法外狂徒张三今年18了"
数字
使用 number
定义数值类型,除了支持十进制和十六进制字面量,还支持二进制和八进制字面量
ts
let decLiteral: number = 6
let hexLiteral: number = 0xf00d
let binaryLiteral: number = 0b1010
let octalLiteral: number = 0o744
let notANumber: number = NaN
let infinityNumber: number = Infinity
布尔
使用boolean
定义类型
ts
let booleand: boolean = true
空值
JavaScript
没有空值Void
概念,Typescript
可以用Void
表示没有任务返回值
ts
function say(): void{
console.log('泰裤辣')
}
用void
表示undefined
和null
ts
let u: void = undefined
let n: void = null
Null 和 Undefined
ts
let u: undefined = undefined
let n: null = null
默认情况下null
和undefined
是所有类型的子类型 。 就是说可以把null
和undefined
赋值给number
类型的变量,也可以赋值给string
类型。
ts
let test: void = undefined
let str: string = '泰裤辣'
str = test // ❌ 不能将类型"void"分配给类型"string"
换成null
或者undefined
就没问题
ts
let test: null = null
let num: number = 2
num = test
注意! 如果tsconfig.json开启了严格模式 null 不能赋值 void 类型
json
{
"compileroptions": {
...
"strict": true
}
}
any、unknown 、never
any
any
任意类型,是一种动态类型,表示变量可以是任意类型的值。不清楚类型的变量类型,使用any
就没有强制限定,随意切换。any
的本质是类型系统中的顶级类型,即 Top Types
在使用 any
类型时,TypeScript
编译器将不会对该变量进行类型检查,允许变量赋值为任何类型的值,以及调用任何方法和属性。
ts
let str: any = '泰裤辣'
str = 123
str = true
或者变量在声明的时候没有制定类型,此时默认的为any
ts
let anys
ayns = 123
ayns = true
开发过程中不知道用什么类型来表示,或者为了通过类型检测时,将类型设置为any
就行,但这样就失去了TS
类型检测的意义。
比如类型不兼容了就 any
一下,类型不想写了也 any
一下,不确定可能会是啥类型还是 any
一下,此时的 TypeScript
就变成了令人诟病的 AnyScript
。
unknown
unknown
类型是 TypeScript 3.0
引入的一种类型,表示变量的类型是未知的。
它和any
一样,所有类型都可以分配给它,但是它不可以赋值给其他类型【any除外】。
ts
let str: unknown = '泰裤辣'
let num: number = 1
let anys: any = false
str = num
num = str // ❌ 不能将类型"unknown"分配给类型"number"。
// ✅ 赋值给 any
anys = str
还有一个区别,unknown
没有办法读任何属性,方法也不可以调用, 换成any
就没问题。
在类型未知的情况下,更推荐使用
unknown
标注。
ts
let person: unknown = {
name: 'xg',
say: () => {
console.log('泰裤辣')
}
}
person.name // ❌ "person"的类型为"未知"
person.say() // ❌ "person"的类型为"未知"
unknown
类型提供了一种更安全的方式来处理未知类型的数据,避免了潜在的运行时错误。
never
never
类型表示永不存在的值的类型,不会携带任何类型信息,因此在联合类型中被直接忽略。
never
类型被称为 Bottom Types
底层类型,是整个类型系统层级中最底层的类型。和 null
、undefined
一样,它是所有类型的子类型。
ts
type A = string | number | never
// 当鼠标移上去发现never类型消失了
type A = string | number
never类型是任何类型的子类型,也可以赋值给任何类型。但除了never本身外,其他类型不可以赋值给never
几个例子
ts
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
return error("Something failed");
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
while (true) {
}
}
never和void区别
-
void
类型只是没有返回值,但本身不会报错。never
类型表示那些永远不会发生的值的类型。当一个函数永远不会返回(例如抛出异常、进入无限循环或遇到不可到达的代码路径),可以将其返回类型标注为never
。tsfunction throwFn(): never { // ❌ 返回"never"的函数不能有可到达的终点 console.log('123') } function throwFn(): void { console.log('123') } function throwFn(): never { throw new Error("An error occurred.") // 永远不会返回 }
-
在联合类型中,
never
会被移除,void
不会tstype A = number | void | never type A = number | void
关键区别:
void
类型表示函数没有返回值或变量没有明确的值。never
类型表示某些情况下函数永远不会返回或出现不可到达的代码路径。
数组(Array)
定义
数组定义有两种方式,第一种,可以在元素类型后面接上阔号Type[]
,表示由此类型元素组成的一个数组:
ts
let list: number[] = [1, 2, 3];
第二种方式是使用数组泛型,Array<Type>
:
ts
let list: Array<string> = ['1', '2', '3'];
多类型数组
数组中每一项的类型都不一样
ts
// 简单粗暴 any
let list: ayn[] = ['泰裤辣', 666, true]
// 使用元组类型
let list2: [string, number, boolean] = ['泰裤辣', 666, true]
对象数组
定义对象类型数组可以使用interface
ts
interface Person {
name: string;
age: number;
email: string;
}
// 声明对象数组
const people: Person[] = [
{ name: "Alice", age: 25, email: "alice@example.com" },
{ name: "Bob", age: 30, email: "bob@example.com" },
{ name: "Charlie", age: 35, email: "charlie@example.com" }
];
多维数组
数组里面套数组该如何定义呢
ts
let list: number[][] = [[1],[2],[3]]
// or
let list2: Array<Array<number>> = [[1],[2],[3]]
类数组
argumentsc
参数是一个类数组和数组类似,但不具备数组相应的方法,这种就不能用普通的方式定义。
ts
function say() {
let arg: any[] = arguments // ❌'arg' is declared but its value is never read.
let arg2: IArguments = arguments
}
元组类型(Tuples)
元组(Tuples)类型是 TypeScript
中用于表示具有固定数量和特定顺序的不同类型的值的数据结构。
元组是固定数量的不同类型的元素组合,和数组不同的是它的元素类型是不固定,而数量固定。
ts
let arr: [string, number, boolean];
arr = ["a", 2, false]; // ✅
arr = [2, "a", false]; // ❌ 不能将类型"number"分配给类型"string"。 不能将类型"string"分配给类型"number"。
arr = ["a", 2]; // ❌ Property '2' is missing in type '[string, number]' but required in type '[string, number, boolean]'
arr[1] = 996
元组定义的数组元素个数都是确定的,类型也固定,当给arr
赋值时,要保证各个位置上的元素类型对应。
接口(Interfaces)
定义
interface
接口,接口是一种用于描述对象的结构和行为的 TypeScript
抽象类型,它可以用来描述较为复杂的数据结构,用接口定义的数据必须要满足约束。
ts
interface Person {
name: string
age: number
}
// ❌ 类型 "{ name: string; }" 中缺少属性 "age",但类型 "Person" 中需要该属性。
// 我们接口里面定义了两个参数,但在实现的时候只有一个参数
let person:Person = {
name: 'zs'
}
// ✅
let person1:Person = {
name: 'zs',
age: 18
}
在函数里面使用
ts
interface Info {
name: string
slogan: string
}
const person = (info: Info):string => {
return `${info.name} 真的是 ${info.slogan}`
}
person({name:'xg', slogan: '泰裤辣'})
多个同名接口可以重合
ts
interface Person {
name: string
age: number
}
interface Person {
sex: boolean
}
// 两个接口Person属性合并起来
let person: Person = {
name: 'zs',
age: 18,
sex: false
}
接口属性
可选属性
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在,在可选属性名字定义的后面加一个?
符号。
ts
interface Person {
name: string
age: number
sex?: boolean
}
// sex 属性可传可不传
let person:Person = {
name: 'zs',
age: 18
}
任意属性
当接口定义好后,只用到一两个数据,但是其他数据太多不想定义,这个时候就可以用任意属性。这里定义了将新增的属性类型定义为 any
如果修改为 string 接口内所有数据类型都必须为 string,也就是索引签名。
ts
interface Person {
name: string
age: number
[propName: string]: any
}
let person:Person = {
name: 'zs',
age: 18,
sex: false,
say: (): string => {
return '泰裤辣'
}
}
只读属性
有些时候定义的字段只能在创建的时候赋值,其他的时候不能修改。就像const
定义的数据不能修改。
ts
interface Person {
readonly name: string
age: number
}
let person:Person = {
name: 'zs',
age: 18,
}
person1.name = 'xg' // ❌ 无法为"name"赋值,因为它是只读属性
函数类型接口
对象里面通常有函数方法,同样也可以在接口里面定义,在借口里定义的函数参数、返回值,在使用的时候必须按照约束传入。
ts
interface Person {
name: string
age: number
say: (slogan: string) => string
}
let person:Person = {
name: 'zs',
age: 18,
say: (slogan) => {
return `我真的是${slogan}`
}
}
person.say(false) // ❌ 类型"boolean"的参数不能赋给类型"string"的参数
person.say('泰裤辣') // ✅
或者这个接口只定义一个函数
ts
interface Fn {
(name: string): number[]
}
const fn: Fn = function(name) {
return '' // ❌ 不能将类型"() => string"分配给类型"Fn"。
return [1]
}
接口继承
A
接口要继承B
接口的属性,可以通过extends
关键字继承
ts
interface Person extends Person2 {
name: string
age: number
}
interface Person2 {
sex: boolean
}
// 两个接口Person属性合并起来
let person: Person = {
name: 'zs',
age: 18,
sex: false
}
接口合并
当同名接口出现时,它们将合并成一个接口。
ts
interface Person {
name: string
age: number
}
interface Person {
sex: boolean
}
// 最后结果会合并
interface Person {
sex: boolean
name: string
age: number
}
object、Object、{}有什么区别
object
object
代表所有非值类型的类型,常用于泛型约束
ts
let obj1: object = 123 // ❌
let obj2: object = '123' // ❌
let obj3: object = false // ❌
let obj4: object = [] // ✅
let obj5: object = {} // ✅
let obj6: object = () => {} // ✅
Object
Object
类型是所有对象 实例的类型,它是在原型链上最顶层,所有值类型引用类型最终都会指向Object
,所以它包含所有类型
ts
let obj1: Object = 123 // ✅
let obj2: Object = '123' // ✅
let obj3: Object = false // ✅
let obj4: Object = [] // ✅
let obj5: Object = {} // ✅
let obj6: Object = () => {} // ✅
{}
它也是支持所有类型,和Object
一样,相当于Object
字面量。
枚举(enums)
枚举(enums)是一种用于定义命名常量集合的数据类型。它们允许我们为一组相关的值分配友好的名称,使代码更易于理解和维护。
使用关键字 enum
后跟枚举名来声明一个枚举。
数字枚举
枚举成员默认从 0 开始自增,可以手动指定值或自定义增长规则。
ts
// 未定义 从 0 开始
enum Status {
'已发布',
'未发布',
'已失效',
}
console.log(Status) // { '已发布': 0, '未发布': 1, '已失效': 2 }
定义了枚举值, 从当前数值开始自增长。
ts
enum Status {
'已发布' = 1,
'未发布',
'已失效',
}
console.log(Status) // { '已发布': 1, '未发布': 2, '已失效': 3 }
一元、二元表达式也可以
ts
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length
}
console.log(FileAccess.ReadWrite) // 6
字符串枚举
当然,枚举值也可以是字符串,此时就没有自增长行为,也可以和数字枚举混合使用。
ts
enum Status {
'已发布' = '1',
'未发布' = '2',
'已失效' = 0
}
接口枚举
在定义接口时使用枚举值,而在实现接口时枚举值要和定义时保持一致,枚举值对应上就行。
ts
enum Status {
'已发布' = '1',
'未发布' = '2',
'已失效' = 0
}
interface Info {
status: Status.已发布
}
let test: Info = {
status: Status.已发布
}
let test2: Info = {
status: Status.已失效 // ❌ 不能将类型"Status.已失效"分配给类型"Status.已发布"。
}
双向映射
生成的代码中,枚举类型被编译成一个对象,它包含双向映射 (name -> value)
和(value -> name)
。
ts
enum Status {
'已发布',
'已失效'= '2'
}
// number 类型
const value1 = Status.已发布
console.log('value', value1) // value 0
const key = Status[value1]
console.log('key', key) // key 已发布
双向映射只对 number 类型生效,字符串无法映射 ,也就是说,枚举值如果是 string
是无法进行映射
ts
enum Status {
'已发布',
'已失效'= '2'
}
// 字符串
const value2 = Status.已失效
console.log('value2', value2) // value2 2
const key2 = Status[value2]
console.log('key2', key2) // key2 undefined
为什么会这样?打印一下Status
看一下
ts
enum Status {
'已发布',
'已失效'= '2'
}
console.log('Status', Status) // Status { '0': '已发布', '已发布': 0, '已失效': '2' }
枚举通过key: value
形式生成一个对象,对于number
类型会多生成一条数据value: key
,所以我们可以通过Status[value]
获取到枚举key
。而字符串类型没有生成,所以无法通过映射获取。
函数(Function)
定义返回值
ts
function add (num1: number, num2: number): number {
return num1 + num2
}
// 箭头函数
const add = (num1: number, num2: number): number => a + b
// 函数表达式
cosnt add = function (num1: number, num2: number): number {
return num1 + num2
}
add(1, 2)
可选参数和默认值
使用问号 ?
来表示可选参数,参数可要可不要。
ts
function add (num1: number, num2?: number): number { // 严格模式下 num2 会报错
return num1 + num2
}
add(1)
默认值,和ES6
用法一样
ts
function add (num1: number, num2: number = 2): number {
return num1 + num2
}
add(1)
剩余参数
ES6 中,可以使用 ...rest
的方式获取函数中的剩余参数(rest
参数)
ts
function add (num1: number, num2: number=2, ...args: any[]): number {
return num1 + num2
}
add(1, 3, 4)
args
类型是依靠剩余参数类型,如果第二位后面剩余参数是已知的,比如string
,就可以改为...args: string[]
函数重载
当函数的参数是多个不同类型,或者返回值不同,就可以使用函数重载。
比如函数 reverse
, 输入数字 123
的时候,输出反转的数字 321
,输入字符串 'hello'
的时候,输出反转的字符串 'olleh'
。
ts
function reverse(x: number): number
function reverse(x: string): string
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''))
} else if (typeof x === 'string') {
return x.split('').reverse().join('')
}
}
类(Class)
TypeScsript
拥有和ES6一样的Class
语法,在此基础上增加了一些新的用法,不熟悉的可以看一下阮一峰老师教程ECMAScript 6 入门-Class
定义
Class
的属性需要先定义后,再赋值,也可以定义默认值
ts
class Person {
name: string;
slogan: string;
sex = false; // 默认值
constructor(name, slogan) {
this.name = name;
this.slogan = slogan;
}
say(): void {
console.log(this.name + this.slogan);
}
}
const p1 = new Person('xg', 123);
修饰符
TypeScript
可以使用三种访问修饰符(Access Modifiers),分别是公共 public
、私有 private
和受保护 protected
,默认的修饰符尾 public
public
public
公有的,可以在任何地方被访问,不受限制,所有属性方法默认的为 public
ts
class Person {
name: string;
slogan: string;
sex = false; // 默认值
constructor(name, slogan) {
this.name = name;
this.slogan = slogan;
}
public say(): void {
console.log(this.name + this.slogan);
}
}
const p1 = new Person('xg', '泰裤辣');
p1.say(); // xg泰裤辣
private
private
私有的,定义的属性方法只能在内部访问,就不能在声明它的类的外部访问,通过 extends
关键字继承的子类中也不可以使用。
ts
class Person {
private name: string;
slogan: string;
sex = false; // 默认值
constructor(name, slogan) {
this.name = name;
this.slogan = slogan;
}
public say(): void {
console.log(this.name + this.slogan);
}
}
// 继承 Person 子类中无法访问 private
class Singer extends Person {
constructor(name, age) {
super(name, age);
}
getPerson(): void {
console.log('name', this.name); // ❌ 属性"name"为私有属性,只能在类"Person"中访问。
}
}
const p1 = new Person('xg', 123);
console.log('name', p1.name); // ❌ 属性"name"为私有属性,只能在类"Person"中访问。
protected
protected
受保护的,和 private
类似,但有一点不同 protected 可以在继承的子类中使用
ts
class Person {
private name: string;
slogan: string;
protected sex = false; // 默认值
constructor(name, slogan) {
this.name = name;
this.slogan = slogan;
}
public say(): void {
console.log(this.name + this.slogan);
}
}
// 继承 Person 父类中 sex 属性可以在子类中访问 但无法在外界使用
class Singer extends Person {
constructor(name, age) {
super(name, age);
}
getPerson(): void {
// 不可以访问 private
console.log('name', this.name);
}
getSex(): void {
// 可以访问 protected
console.log('sex', this.sex);
}
}
const p1 = new Person('xg', '泰裤辣');
p1.say(); // xg泰裤辣
const p2 = new Singer('ly', '再靠近我一点');
p2.getSex(); // sex false
// 类的外面访问 protected 属性
p2.sex; // ❌ 属性"sex"受保护,只能在类"Person"及其子类中访问。
readonly修饰符
readonly
将属性设置为只读的,只读属性必须在声明时或构造函数里被初始化,并不能在外界修改,只允许出现在属性声明或索引签名或构造函数中。
ts
class Person {
private name: string;
slogan: string;
protected sex = false; // 默认值
readonly id = 1; // 只读属性
constructor(name, slogan) {
this.name = name;
this.slogan = slogan;
}
public say(): void {
// readonly可以访问
console.log('id', this.id); // id 1
console.log(this.name + this.slogan);
}
readonly sing(): void { // ❌ "readonly" 修饰符仅可出现在属性声明或索引签名中。
console.log(this.name + '在唱歌');
}
}
const p1 = new Person('xg', '泰裤辣');
console.log('p1.id', p1.id); // 1.id 1
p1.id = '2'; // ❌ 无法为"id"赋值,因为它是只读属性。
注意 如果 readonly
和其他访问修饰符同时存在的话,需要写在其后面。
ts
class Person {
private readonly name: string;
slogan: string;
sex = false; // 默认值
id = 1;
constructor(name, slogan) {
this.name = name;
this.slogan = slogan;
}
}
存取器
TypeScript
支持通过getters/setters
来截取对对象成员的访问,可以改变属性的赋值和读取行为。
ts
class Person {
constructor(name) {
this.name = name; // 这里有一次赋值 也会触发 setter 也就是 set name 方法
}
get name(): string {
return 'return name';
}
set name(value: string) {
// 执行两次 一次在 constructor 里赋值 一次在外面 p1.name = '234'
// set name xg
// set name 234
console.log('set name', value);
}
}
const p1 = new Person('xg');
p1.name = '234';
console.log('get p1.name', p1.name); // get p1.name return name
static 静态属性和静态方法
static
修饰符定义的属性方法,不可以通过 this
取访问,只能通过类名去调用,不需要实例化。
ts
class Person {
static id = '123';
constructor() {
this.say(); // ❌ 属性"say"在类型"Person"上不存在。你的意思是改为访问静态成员"Person.say"吗?
}
static say(slogan): void {
console.log(slogan);
console.log('id', Person.id); // id 123
}
}
Person.say('泰裤辣'); // 泰裤辣
console.log('Person.id', Person.id); // Person.id 123
抽象类
使用abstract
关键字定义类为抽象类,定义的方法都是抽象方法。
- 抽象方法不能实现,只能定义描述,类似接口一样。
- 抽象类不能直接杯实例化。它的实例化是毫无意义的,只能基于派生类中去实现。当抽象类中有抽象方法时,必须要实现。
抽象类(Abstract Classes):它是一种不能直接实例化的类,只能被继承。
ts
abstract class Person {
name: string
}
const p1 = new Person(); // ❌ 无法创建抽象类的实例
在派生类中实现抽象类
ts
abstract class Person {
name: string;
constructor(name?: string) {
this.name = name;
}
getName(): string {
return this.name;
}
// 抽象方法
abstract init(name: string): void;
}
class User extends Person {
constructor() {
super();
}
init(name: string): void {
this.name = name;
}
}
const u1 = new User();
u1.init('xg');
console.log('getName', u1.getName()); // getName xg
类实现接口
以通过 implements
关键字来实现接口,强制类遵循接口的契约。可以使用关键字implements
后面跟多个interface
的名字,用逗号隔开。
ts
interface Person1 {
name: string;
}
interface Person2 {
age: number;
}
interface Person3 {
say: (slogan: string) => void;
}
class Person implements Person1, Person2, Person3 {
age: number;
name: string;
constructor(name: string, age: number) {
this.age = age;
this.name = name;
}
say(slogan) {
console.log(this.name + slogan);
}
}
const p1 = new Person('xg', 18);
p1.say('泰裤辣');
命名空间(NameSpace)
Namespace
用于组织和管理代码,防止全局命名冲突,如果两个模块名称相同,就会报错。
TypeScript
和JavaScript
一样,都是使用ES5
模块,也就是通过export import
形式进行导入导出。任何一个包含顶级export import
的文件都会当成一个模块,相反,如果一个文件不带顶级export import
声明,也会被视为全局可见的模块。

使用关键字 namespace
来声明一个命名空间。,将数据包裹到一个命名空间内,而不是放在全局下。 使用 export
关键字将命名空间中的成员(变量、函数、类等)导出,使其可以在外部访问。
ts
// file1.ts
namespace A {
export const a = 1
}
// file2.ts
namespace B {
export const a = 2
}
最后的编译结果就是将命名空间A
变成一个对象,导出的数据就为A
下面的属性。
ts
var A;
(function (A) {
A.a = 2;
})(A || (A = {}));
console.log('A', A.a);
命名空间可以使用 /// <reference>
指令或模块化的导入语法来使用命名空间中的成员。
ts
/// <reference path="file1.ts.ts" />
console.log(A.a)
嵌套命名空间
命名空间可以嵌套在其他命名空间内部,形成层级结构,以更好地组织和管理代码。
ts
namespace A {
export namespace B {
export const a = 2
}
}
import TMP = A.B
console.log('B', A.B.a ) // B 2
console.log('TMP',TMP.a ) // TMP { a: 2 }
合并
命名空间也可以像接口那样合并
ts
namespace A {
export const a = 1
}
namespace A {
export const b = 2
}
console.log('A.b', A.b) // A.b 2
TypeScript 进阶
类型注解和类型推断
在 TypeScript
中,类型注解(Type Annotation)和类型推断(Type Inference)是用来确定变量、函数和表达式的类型的两种方法。
类型注解
类型注解是明确地给变量、函数参数、函数返回值或表达式指定类型的方式。使用 :
后跟类型名称来注解。
ts
let age: number = 30;
function greet(name: string): string {
return "Hello, " + name;
}
在上述示例中,age
被注解为 number
类型,name
参数被注解为 string
类型,函数 greet
的返回值被注解为 string
类型。
类型推断
类型推断是 TypeScript
的一种功能,根据变量的赋值表达式自动推断出变量的类型。TypeScript
根据上下文和赋值的类型推断出变量的类型,而无需显式注解。

在上面例子中,str
被推断为string
类型,所以在修改的时候不能赋值为其他类型,只能是string
。
如果声明了变量,但是没有任何赋值、定义类型操作,默认的为any
类型

泛型(Generics)
当数据类型不是固定的时候,可以使用泛型将数据类型变成动态的。也就是先不定义数据的一个具体类型,当使用的时候再指定。
函数里的泛型
在函数名后面跟<T>
,其中T
就是动态的类型,形参value
的类型也就是T
,返回的也是一个T
类型的数组。
当调用函数的时候,在函数名后面传入需要定义的数据类型<string>
,此时函数内部所有T
都会被替换成string
。
泛型可以定义多个
ts
function getData<T, K>(value1: T,value2: K): Array<T | K> {
return [value1, value2]
}
getData(1, 2)
getData<string, number>("3", 4)
getData(true, null)
这里没有写,应为通过类型推断,已经知道了类型。

泛型接口
在interface
或者type
中也是可以使用泛型,用法一样。
ts
interface ResData<T>{
code: number,
msg: string
data: T
}
const response: ResData<boolean> = {
code: 1,
msg: 'success',
data: false
}
type Data<K> = number | string | K
const data: Data<null> = null
泛型变量
看别人的ts
代码会看到很多泛型T、E、V...之类的变量,为什么有的地方是T,有的地方是V。TypeScript
在这方面没有强制要求,都是开发者们为了方便阅读理解,默认形成的一套规范。
常用的单个字母泛型变量:
- T:表示一般的泛型类型参数。
- K:表示对象中的键类型。
- V:表示对象中的值类型。
- E:表示数组或元组中的元素类型。
- R:表示函数的返回类型。
- S, U, V, ...:表示额外的泛型类型参数。
泛型约束
在函数内部使用泛型时,还不知道是什么类型,也就不能使用操作数据的属性或方法。
ts
function test<T>(name: T): void {
console.log('name length', name.length) // ❌ 类型"T"上不存在属性"length"。
}
此时就可以使用泛型约束,对使用的泛型数据进行约束,约束为具有length
属性的类型,如string
、Array
。
语法:< T extends 类型 >
ts
interface Len {
length: number
}
function test<T extends Len>(name: T): void {
console.log('name length', name.length)
}
test('1')
test([1,2,3])
test(2) // ❌ 类型"number"的参数不能赋给类型"Len"的参数
test(false) // ❌ 类型"boolean"的参数不能赋给类型"Len"的参数。
高级类型
交叉类型
多种类型的集合,使用&
符号,联合对象将具有所有联合类型的所有成员,类似接口继承和接口合并。
ts
interface Person {
name: string
age: number
}
interface Person2 {
sex: boolean
}
let person: Person & Person2 = {
name: 'zs',
age: 18,
sex: false
}
联合类型
当数据有多个类型时可以使用
ts
let id: string | number
id = 123
id = '123'
id = false // ❌ 不能将类型"boolean"分配给类型"string | number"
使用联合类型的属性,只能访问此联合类型的所有类型里共有的属性或方法
ts
let id: string | number
id = '123'
id = 123
id.length // ❌ 类型"number"上不存在属性"length"
索引签名
当类型的属性的实际名称是未知的,数据类型也是未知的,就可以使用索引签名。索引签名一般用来新增数据。
ts
type User = {
name: string
[key: string]: any // 索引签名
}
const user: User ={
name: 'xg',
age: 12
}
需要注意的是索引签名中定义好了数据的类型,必须要和新增的数据类型对应上。
ts
interface User {
name: string
[key: string]: string // 索引签名
}
const user: User ={
name: 'xg',
age: 12 // ❌ 不能将类型"number"分配给类型"string"
}
keyof 和 typeof
keyof
keyof
是 TypeScript
中的一个操作符,用于获取对象类型或接口类型的键集合。它返回一个联合类型,包含给定类型中所有可用的键。
ts
type Person = {
name: string;
age: number;
address: string;
};
type PersonKeys = keyof Person; // "name" | "age" | "address"
keyof类型约束
可以使用 keyof
来约束函数参数或泛型类型参数,以确保传入的值是给定类型的有效键。
ts
function getProperty(obj: any, key: keyof typeof obj) {
return obj[key];
}
const person = {
name: "John",
age: 30,
address: "123 Street",
};
const name = getProperty(person, "name"); // 类型推断为 string
typeof
typeof
操作符用于获取一个值的类型。它允许我们在编译时获取一个值的类型信息,以便进行类型推断、类型声明或类型操作。
ts
const p = {
name: 'CJ',
age: 18
};
type Person = typeof p;
// 等同于
type Person = {
name: string;
age: number;
}
keyof和typeof一起使用
keyof
和 typeof
可以结合使用,以实现更复杂的类型操作和类型推断。
比如要获取某个对象的key
作为类型,可以先通过typeof
获取到这个对象的类型,再通过keyof
将key
设为类型。
ts
const person = {
name: 'xg',
age: 18,
}
type personType = keyof typeof person // type personType = "name" | "age"
let p1: personType
p1 = 'name' // ✅
p1 = 'age' // ✅
p1 = 'sex' // ❌ 不能将类型""sex""分配给类型""name" | "age""。
同理,也可以用来规范某个枚举对象的key
ts
enum MsgEnum {
'success' = '请求成功',
'error' = '请求失败',
'warring' = '请重试',
}
type msgType = keyof typeof MsgEnum // type msgType = "success" | "error" | "warring"
let msg: msgType
msg = 'success' // ✅
msg = 'error' // ✅
msg = 'test' // ❌ 不能将类型""test""分配给类型""success" | "error" | "warring""。
类型保护
类型保护是一种在 TypeScript
中确定变量类型的机制,以便在编程过程中执行相应的类型操作。通过类型保护,可以在代码中根据条件判断来缩小变量的类型范围,以便进行更精确的类型操作。
typeof类型保护
使用 typeof
关键字可以进行类型保护,根据变量的类型进行条件判断。
ts
function printValue(value: string | number) {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}
注意事项:
typeof
类型保护只能用于进行基本类型的判断,如string
、number
、boolean
等。- 对于复杂类型,如对象、数组等,
typeof
类型保护无法进行精确判断。
instanceof 类型保护:
使用 instanceof
关键字可以进行类型保护,检查对象是否属于特定的类或构造函数。
ts
class Person {
// ...
}
class Animal {
// ...
}
function printName(obj: Person | Animal) {
if (obj instanceof Person) {
console.log(obj.name);
} else {
console.log(obj.species);
}
}
自定义类型保护函数
可以编写自定义的类型保护函数,根据特定的条件判断来确定变量的类型。
ts
function isString(value: unknown): value is string {
return typeof value === "string";
}
function printLength(value: unknown) {
if (isString(value)) {
console.log(value.length);
}
}
类型别名(type)
类型别名是为现有类型创建一个新名字 ,可以通过关键字 type
来定义。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,交叉类型,元组以及其它任何你需要手写的类型。
ts
type type1 = string // 原始类型
type type2 = number
type type3 = (name: string) => number // 函数
type type4 = string | type3 // 联合类型
type type5 = string & boolean // 交叉类型
type type6 = [number] // 数组
const str: type1 = '123'
const idCard: type4 = '111111'
type和interface的区别
type
无法使用extends
关键词继承其他type
,interface
可以,只能使用&
交叉类型和在一起。interface
遇到重名的会进行合并,type
不会。
高级用法
使用extends
关键字,左边值会遵循右边值的子类型,可以用三元表达式来判断,决定它最后的类型。
看个例子理解一下,最后type1
的类型为boolean
ts
type type1 = string extends any ? string : number // type type1 = boolean
同理,我们可以吧extends
右边值换成其他的,猜一下type2
最后的类型。
ts
type type2 = 1 extends never ? number : string // type type2 = string
它的判断规则遵循TypeScript
数据类型的包含关系
- 顶级类型
any
、unknown
,其他所有类型都是他们的子类。 Object
类型是所有对象实例的类型,所有类型的原型链顶端都指向它Number
、String
、Boolean
等这些,都是包装类型,在Js中也存在number
、string
、boolean
TypeScript提供的原始类型1
、'123'
、false
都是字面量never
底层类型
类型断言(Type Assertion)
类型断言是 TypeScript
中的一种语法,用于告诉编译器某个值的具体类型。可以用来手动指定一个值的类型,就像类型转换,将某个值强制转换为特定类型。
语法:
- 值 as 类型
value as number
- <类型>值
ts
function fn(value: string | number): number {
return (value as string).length
}
fn(123123)
注意事项:
- 类型断言是告诉编译器的一种方式,它在编译时起作用,不会影响运行时的类型。
- 需要注意的是,使用类型断言时要确保断言的类型是正确的,否则可能会导致运行时错误。
- 如果无法确定断言的类型是否正确,可以使用类型保护机制来进行更安全的类型判断。
symbol类型
在ES5
中添加了一个新的原始类型symbol
,通过它定义的数据是不可改变且唯一的 。创建方式通过Symbol
构造函数创建的。
定义
ts
let sym1 = Symbol()
let sym2 = Symbol('key') // 可选字符
唯一的
ts
let sym1 = Symbol()
let sym2 = Symbol()
sym1 === sym2 // false
作为对象的key
像字符串一样,symbol也可以被用做对象属性的键。由于symbol
的唯一性,在对象里使用symbols
作为key
也是唯一的,避免属性key
重复问题。
ts
let sym = Symbol()
let obj = {
name: 'xg'
[sym]: 123
}
那如何获取对象中用symbol
定义的key
呢,普通的对象方法都获取不到,可以使用Reflect.ownKeys()
方法。
ts
let sym = Symbol()
let obj = {
name: 'xg',
[sym]: 123
}
console.log( Reflect.ownKeys(obj)) // [ 'name', Symbol() ]
遍历对象中的Symbol
只遍历symbol
要遍历对象中的 Symbol
属性,可以使用 Object.getOwnPropertySymbols()
方法。该方法返回一个包含对象所有 Symbol
属性的数组。使用遍历循环或其他迭代方法来处理这个数组。【Object.getOwnPropertySymbols() 方法仅返回对象自身的 Symbol 属性,不包括继承的属性。】
ts
const name = Symbol('name')
const age = Symbol('age')
const person = {
[name]: 'xg',
[age]: 18,
sex: false,
}
// 获取对象中的所有 Symbol 属性
const symbols = Object.getOwnPropertySymbols(person)
console.log('symbols', symbols) // symbols [ Symbol(name), Symbol(age) ]
// 遍历 Symbol 属性数组
symbols.forEach((symbol) => {
console.log(symbol, person[symbol])
// Symbol(name) xg
// Symbol(age) 18
})
遍历所有key
如果需要同时遍历对象的 Symbol
属性和其他属性,您可以使用 Reflect.ownKeys()
方法。该方法返回一个包含对象所有属性(包括 Symbol
和非 Symbol
属性)的数组。
ts
const name = Symbol('name')
const age = Symbol('age')
const person = {
[name]: 'xg',
[age]: 18,
sex: false,
}
// 获取对象中的所有属性(包括 Symbol 和非 Symbol 属性)
const keys = Reflect.ownKeys(person)
console.log('keys', keys) // keys [ 'sex', Symbol(name), Symbol(age) ]
// 遍历所有属性
keys.forEach((key) => {
console.log(key, person[key])
// sex false
// Symbol(name) xg
// Symbol(age) 18
})
三斜杠指令
三斜线指令是包含单个XML标签的单行注释。 注释的内容会做为编译器指令使用。三斜线指令仅可放在包含它的文件的最顶端。
三斜线引用告诉编译器在编译过程中要引入的额外的文件,会将依赖的文件编译到同一个文件当中。
如果在tsconfig.json
中指定了noResolve: true
,就会忽略三斜线指令。
导入声明文件
语法:/// <reference path="..." />
在node types
文件里,引入了各种模块的声明文件
ts
/// <reference path="http.d.ts" />
/// <reference path="http2.d.ts" />
/// <reference path="https.d.ts" />
导入第三方包
语法:/// <reference types="node" />
比如导入一下node
的数据类型,先安装一下npm i @type/node -D
ts
/// <reference types="node"/>
声明文件.d.ts
当使用第三方库时,需要引入它的声明文件才能获得对应的代码补全等提示功能。
有些三方库内部已经有了,可以直接使用,如果没有就需要另外安装。
比如express
库,源码里面是没有声明文件,所有要安装对应的声明文件
bash
npm i --save-dev @types/express
三方声明文件安装规则,都是以@type/
开头,后面接库的名称,也可以在npm types上搜索对应的声明文件。
如果三方声明文件库都没有,就要手动书写声明文件,文件后缀名为.d.ts
declare
关键字可以将定义的类型暴露出来
ts
declare var 声明全局变量
declare function 声明全局方法
declare class 声明全局类
declare enum 声明全局枚举类型
declare namespace 声明(含有子属性的)全局对象
interface 和 type 声明全局类型
配置文件详解
创建TypeScript
配置文件tsconfig.json
,添加 --locale zh-CN
参数生产的文件注释都是中文
bash
tsc --init --locale zh-CN
json
{
"compilerOptions": {
/* 请访问 https://aka.ms/tsconfig,了解有关此文件的详细信息 */
/* 项目 */
// "incremental": true, /* 保存 .tsbuildinfo 文件以允许项目增量编译。 */
// "composite": true, /* 启用允许将 TypeScript 项目与项目引用一起使用的约束。 */
// "tsBuildInfoFile": "./.tsbuildinfo", /* 指定 .tsbuildinfo 增量编译文件的路径。 */
// "disableSourceOfProjectReferenceRedirect": true, /* 在引用复合项目时禁用首选源文件而不是声明文件。 */
// "disableSolutionSearching": true, /* 在编辑时选择项目退出多项目引用检查。 */
// "disableReferencedProjectLoad": true, /* 减少 TypeScript 自动加载的项目数。 */
/* 语言和环境 */
"target": "es2016", /* 为发出的 JavaScript 设置 JavaScript 语言版本并包含兼容的库声明。 */
// "lib": [], /* 指定一组描述目标运行时环境的捆绑库声明文件。 */
// "jsx": "preserve", /* 指定生成的 JSX 代码。 */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* 为源文件中的修饰声明发出设计类型元数据。 */
// "jsxFactory": "", /* 指定在将 React JSX 发出设定为目标时要使用的 JSX 中心函数,例如 "react.createElement" 或 "h"。 */
// "jsxFragmentFactory": "", /* 指定在将 React JSX 发出设定为目标时用于片段的 JSX 片段引用,例如 "React.Fragment" 或 "Fragment"。 */
// "jsxImportSource": "", /* 指定使用 "jsx: react-jsx*" 时用于导入 JSX 中心函数的模块说明符。 */
// "reactNamespace": "", /* 指定为 "createElement" 调用的对象。这仅适用于将 "react" JSX 发出设定为目标的情况。 */
// "noLib": true, /* 禁用包括任何库文件(包括默认的 lib.d.ts)。 */
// "useDefineForClassFields": true, /* 发出符合 ECMAScript 标准的类字段。 */
// "moduleDetection": "auto", /* 控制用于检测模块格式 JS 文件的方法。 */
/* 模块 */
"module": "commonjs", /* 指定生成的模块代码。 */
// "rootDir": "./", /* 指定源文件中的根文件夹。 */
// "moduleResolution": "node10", /* 指定 TypeScript 如何从给定的模块说明符查找文件。 */
// "baseUrl": "./", /* 指定基目录以解析非相关模块名称。 */
// "paths": {}, /* 指定一组将导入重新映射到其他查找位置的条目。 */
// "rootDirs": [], /* 允许在解析模块时将多个文件夹视为一个文件夹。 */
// "typeRoots": [], /* 指定多个行为类似于 "./node_modules/@types" 的文件夹。 */
// "types": [], /* 指定要包含的类型包名称,而无需在源文件中引用。 */
// "allowUmdGlobalAccess": true, /* 允许从模块访问 UMD 变量全局。 */
// "moduleSuffixes": [], /* 解析模块时要搜索的文件名后缀列表。 */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* 启用导入 .json 文件。 */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* 禁止 "import"、"require" 或 "<reference>" 扩展 TypeScript 应添加到项目的文件数。 */
/* JavaScript 支持 */
// "allowJs": true, /* 允许 JavaScript 文件成为程序的一部分。使用 "checkJS" 选项从这些文件中获取错误。 */
// "checkJs": true, /* 在已检查类型的 JavaScript 文件中启用错误报告。 */
// "maxNodeModuleJsDepth": 1, /* 指定用于从 "node_modules" 检查 JavaScript 文件的最大文件夹深度。仅适用于 "allowJs"。 */
/* 发出 */
// "declaration": true, /* 从项目的 TypeScript 和 JavaScript 文件生成 .d.ts 文件。 */
// "declarationMap": true, /* 为 d.ts 文件创建源映射。 */
// "emitDeclarationOnly": true, /* 仅输出 d.ts 文件,而不输出 JavaScript 文件。 */
// "sourceMap": true, /* 为发出的 JavaScript 文件创建源映射文件。 */
// "inlineSourceMap": true, /* 在发出的 JavaScript 中包括源映射文件。 */
// "outFile": "./", /* 指定将所有输出捆绑到一个 JavaScript 文件中的文件。如果 "declaration" 为 true,还要指定一个捆绑所有 .d.ts 输出的文件。 */
// "outDir": "./", /* 为所有已发出的文件指定输出文件夹。 */
// "removeComments": true, /* 禁用发出注释。 */
// "noEmit": true, /* 禁用从编译发出文件。 */
// "importHelpers": true, /* 允许每个项目从 tslib 导入帮助程序函数一次,而不是将它们包含在每个文件中。 */
// "importsNotUsedAsValues": "remove", /* 指定仅用于类型的导入的发出/检查行为。 */
// "downlevelIteration": true, /* 发出更合规但更详细且性能较低的 JavaScript 进行迭代。 */
// "sourceRoot": "", /* 指定调试程序的根路径以查找引用源代码。 */
// "mapRoot": "", /* 指定调试程序应将映射文件放置到的位置而不是生成的位置。 */
// "inlineSources": true, /* 在发出的 JavaScript 内的源映射中包含源代码。 */
// "emitBOM": true, /* 在输出文件的开头发出一个 UTF-8 字节顺序标记(BOM)。 */
// "newLine": "crlf", /* 设置发出文件的换行符。 */
// "stripInternal": true, /* 禁用在其 JSDoc 注释中包含 "@internal" 的发出声明。 */
// "noEmitHelpers": true, /* 在已编译输出中禁用生成自定义帮助程序函数(如 "__extends")。 */
// "noEmitOnError": true, /* 禁用在报告了任何类型检查错误时发出文件。 */
// "preserveConstEnums": true, /* 在生成的代码中禁用擦除 "const enum" 声明。 */
// "declarationDir": "./", /* 指定已生成声明文件的输出目录。 */
// "preserveValueImports": true, /* 保留 JavaScript 输出中未使用的导入值,否则将删除这些值。 */
/* 互操作约束 */
// "isolatedModules": true, /* 确保可以安全地转译每个文件,而无需依赖其他导入。 */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* 当模块没有默认导出时,允许"从 y 导入 x"。 */
"esModuleInterop": true, /* 发出其他 JavaScript 以轻松支持导入 CommonJS 模块。这将启用 "allowSyntheticDefaultImports" 以实现类型兼容性。 */
// "preserveSymlinks": true, /* 禁用将符号链接解析为其实际路径。这会关联到节点中的同一标志。 */
"forceConsistentCasingInFileNames": true, /* 确保导入中的大小写正确。 */
/* 类型检查 */
"strict": true, /* 启用所有严格类型检查选项。 */
// "noImplicitAny": true, /* 对具有隐式 "any" 类型的表达式和声明启用错误报告。 */
// "strictNullChecks": true, /* 进行类型检查时,请考虑 "null" 和 "undefined"。 */
// "strictFunctionTypes": true, /* 分配函数时,请检查以确保参数和返回值与子类型兼容。 */
// "strictBindCallApply": true, /* 检查 "bind"、"call" 和 "apply" 方法的参数是否与原始函数匹配。 */
// "strictPropertyInitialization": true, /* 检查是否有已声明但未在构造函数中设置的类属性。 */
// "noImplicitThis": true, /* 在 "this" 的类型为 "any" 时启用错误报告。 */
// "useUnknownInCatchVariables": true, /* 将 catch 子句变量默认为 "unknown" 而不是 "any"。 */
// "alwaysStrict": true, /* 请确保始终发出 "se strict"。 */
// "noUnusedLocals": true, /* 在未读取局部变量时启用错误报告。 */
// "noUnusedParameters": true, /* 在未读取函数参数时引发错误。 */
// "exactOptionalPropertyTypes": true, /* 将可选属性类型解释为已写,而不是添加 "undefined"。 */
// "noImplicitReturns": true, /* 为未在函数中显式返回的代码路径启用错误报告。 */
// "noFallthroughCasesInSwitch": true, /* 为 switch 语句中的故障案例启用错误报告。 */
// "noUncheckedIndexedAccess": true, /* 使用索引访问时,将 "undefined" 添加到类型。 */
// "noImplicitOverride": true, /* 确保使用替代修饰符标记派生类中的替代成员。 */
// "noPropertyAccessFromIndexSignature": true, /* 对使用索引类型声明的键强制使用索引访问器。 */
// "allowUnusedLabels": true, /* 对未使用的标签禁用错误报告。 */
// "allowUnreachableCode": true, /* 对无法访问的代码禁用错误报告。 */
/* 完成度 */
// "skipDefaultLibCheck": true, /* 跳过 TypeScript 附带的类型检查 .d.ts 文件。 */
"skipLibCheck": true /* 跳过对所有 .d.ts 文件的类型检查。 */
},
"include": ["src/**/*.ts", "demo.ts"] , /* 指定包含的目录、文件*/
"exclude": ["test.ts"], /* 排除的目录、文件*/
}
相关文档
- TypeScript中文手册
- 1.2W字 | 了不起的 TypeScript 入门教程
- TypeScript 入门教程
- (三万字长文)类型即正义:TypeScript 从入门到实践系列,正式完结!
- Playground 在线ts
- ECMAScript 6 入门