看到身边的朋友在工作中都用上了
TypeScript
,都直呼真香!而我从学习和做
TypeScript
的项目到现在也有几个月了,也做了一些总结。本文和你们一起分享我的总结!
TypeScript编译器
通常情况下,编译器拿到一段代码后,会转换成抽象句法树(
AST
)。然后把
AST
转成字节码。字节码再传给运行时程序计算。最终得到结果。步骤如下:
把程序解析成
AST
。
把
AST
编译成字节码。
运行时计算字节码。
TS并不是直接编译成字节码,而是先编译成JS代码。然后像以上步骤那样,在浏览器或
node
运行得到JS代码。
说到这,可能有些同学就问了。
TS
是怎么保证代码安全的呢?
至关重要的一个步骤就是:TS编译器生成
ATS
后,在真正运行代码之前,TS会对代码做类型检查。TS的编译过程大致如下图。
TSC
把TS编译成JS代码时,是不会考虑类型的。类型只在类型检查这步使用。
类型系统分两种。
通过显式句法告诉编译器所有值的类型。
自动推导值的类型。
这两种类型系统TS都具备,可以显式注解类型,也可以让TS推导多数类型。
let num: number = 1;
let isShow: boolean = false;
let name: string = "图图";
let n = 2;
let b = true;
let m = ["美美", "牛爷爷"];
TypeScript类型大全
下面列出一个TS中的类型结构图。
any
是个兜底类型(表示所有类型),声明any
类型的变量后,你可以对它做任何的操作。就跟平时写的JS代码没区别。
let a: any = 1
a = false
a = '图图'
a = [1, 2, 3, 4]
a = a.join('')
a = a.split('')
如果使用any
类型,类型检查器就发挥不了其作用,导致运行时抛出错误。我们要尽量避免使用any
类型。
unknown
unknown
类型是为了解决any
类型的缺陷,unknown
也可以表示所有值。当你不知道一个值的类型时,又不使用any
的情况下,可以使用unknown
类型。但TS
会要求你做二次检查。
let num: unknown = 1
let isNum = num === 2
let addNum = num + 100
if (typeof num === 'number') {
let a = num + 100
console.log(d)
以上示例中,unknown
类型的值可以做比较(isNum
)。但是不能直接性对值进行算术运算操作(addNum
)。要先检查这个值的确是某个类型(a
),上面使用了typeof
运算符对num
变量的值进行二次确认。除了typeof
运算符,还可以用instance
运算符。
boolean
boolean
类型只有两个值:true
和false
。
let isShow = true
let isHide: boolean = false
number
number
类型包括所有数字。
let num: number = 1
let addNum = 2
bigint
bigint
类型是JS原有的类型。处理比较大的数时才用到。
let big1 = 1234n
let big2: bigint = 5678n
string
let name = '图图'
let sex: string = '男'
symbol
symbol
类型是ES6加入的类型。在实际开发中,不太常用。没学ES6的同学,建议看阮一峰老师的ES6书籍。
let names: symbol = Symbol('美美')
let height = Symbol('180')
const weight: unique symbol = Symbol('55')
let g: unique symbol = Symbol('55')
TypeScript为symbol
进行了补充,加入了unique symbol
类型。在定义这种类型的值时,只能用const
而不是let
。在Vs Code
编辑器中会显示typeof VariableName
,而不是unique symbol
。
null和undefined
在Typescript中,null
和undefined
也有各自的类型。类型名称也是null
和undefined
。
这两个类型比较特殊,在TypeScript中,undefined
类型只有undefined
一个值,null
类型只有null
一个值。
let a: undefined = undefined
let b: null = null
never和void
除了undefined
和null
以外,还有never
和void
类型。这两个类型都有着明确的作用。
void
是函数没有显式返回任何值时的返回类型。
never
是函数根本不返回时使用的类型。(比如函数执行过程中报错了,或者永远运行下去)
function addNum(): void {
let a = 1 + 1
let b = a * a
function a() {
throw TypeError('总是报错')
function b() {
while (true) {
console.log('我在无限循环')
如果说unknown
是其他每个类型的父类型,那never
就是其他每个类型的子类型。可以把never
理解成 “兜底类型”。这就说明,never
类型可赋值给其他任何类型,在任何地方都可以放心使用never
类型的值。
需要注意的是,在使用never
类型时。也要像unknown
类型那样,对其进行二次检查。
元组是array
的子类型,用于定义数组的一种特殊方式,长度固定,索引位置上的值具有固定的已知类型。声明元组时必须显式注解类型。因为创建元组用的句法和数组一样,都是方括号。TS
遇到方括号,都会认为是数组类型。
let nums: [number] = [1]
let personInfo: [string, number, string] = ['图图', 24, '1998']
元组也支持可选元素和剩余元素。
let nums: [number, number?][] = [[100], [200, 259], [300]]
let names: [string, ...string[]] = ["图图", "小美", "牛爷爷", "图爸爸"]
let list: [number, boolean, ...Object[]] = [1, true, { type: "Object" }, { name: "图图" }]
枚举是一种无序数据结构,用于列举该类型中的所有值。把key
(键)映射到value
(值)中。枚举有两种形式:字符串跟字符串之间的映射和字符串跟数字之间的映射。
enum Fruits {
Apple,
Banana,
Orange
console.log(Fruits.Apple)
console.log(Fruits['Orange'])
访问枚举的值有两种方式,方括号[]
或点号.
都可以。不同的方式访问,所得到的值也不同。
TS还会自动推算枚举中每个成员对应的数字,也可以自己手动设置。
enum Car {
Audi = 1,
Honda = 2,
ToYoTa = 3
console.log(Car[1])
console.log(Car.ToYoTa)
一个枚举可以分几次声明,TS会自动将每一部分合并在一起。
enum Fruits {
Apple = 0,
Banana = 1,
Orange = 2
enum Fruits {
Watermelon = 3
console.log(Fruits)
但要注意的是,分开声明的话TS
只会推导其中一部分的值。最好给枚举的每个键都显式赋值。如果把上面的Watermelon
的值去掉,它的值就为0
。
键的值可以通过计算得出,而且不必给所有的键都赋值。TS
会推导出缺失的值。
enum Fruits {
Apple = 10,
Banana = 10 + 1,
Orange
console.log(Fruits)
'10': 'Apple',
'11': 'Banana',
'12': 'Orange',
Apple: 10,
Banana: 11,
Orange: 12
上面代码中,Orange
键没有赋值。TS
自动推导出Orange
的值为12
,它的前一个键的值为11
。这种行为只能是值为数字类型的情况下才会出现。
let nums = [1, 2, 3]
let names: string[] = ['图图', '牛爷爷', '图妈妈']
let fruits: Array<string> = ['apple', 'banana', 'orange']
TS
中的对象类型表示对象的结构。JS
一般采用结构化类型,TS
直接沿用。
结构化类型:只关心对象有哪些属性,而不管属性使用什么名称。也叫做鸭子类型(即不以貌取人)。
在TS
中声明对象类型有几种方式。
object
第一种,把一个值声明为object
类型:
let a: object = {
b: '1111'
console.log(a.b)
这种方式声明一个对象,只能表示该值是个对象。而做不了任何操作。
对象字面量
第二种,让TS
推导该对象的结构。也就是对象字面量句法。
let person = {
name: '图图',
age: 24
也可以在花括号内明确描述。
let person: { name: string, age: number } = {
name: '小美',
age: 18
对象字面量句法的意思是:这个东西的结构是这样的。
{ name: string, age: number }
描述的是一个对象的结构,上面的例子中的对象字面量满足了该结构,如果添加额外的属性或缺少必要的属性时,就会报错。
let person: { name: string, age: number } = {
name: '图图',
age: 24
person.height = 175
默认情况下,TS
对对象的属性要求非常严格。如果声明对象有个类型为number
的属性age
,TS
将预期对象有这么一个属性,而且也只有这一个属性。如果缺少age
属性,或者多了其他属性,就会报错。
针对这种情况,后面会讲到可选属性和索引签名。
空对象类型
对象字面量表示还有一种:空对象类型({}
)。除了null
和undefined
以外的任何类型都可以赋值给空对象类型,用起来比较复杂,建议不要使用这种方式。
let car: {}
car = {}
car = { name: 'audi' }
car = []
car = 'ToYoTa'
Object
最后一种声明对象类型的方式:Object
。和{}
的作用一样的。不推荐使用。
通常情况下,只推荐前两种方式声明对象类型。如果对对象的字段没有要求,那么就使用第一种。如果想知道对象有哪些字段,或者对象的值都为相同的类型,就使用第二种。
类型字面量
我们先来看一段代码。
let show: true = true
let disable: false = true
可以看到show
的类型并不是普通的boolean
类型,而是只为true
的boolean
类型。这就是类型字面量。而disabele
变量的类型为false
,但值确是true
。此时,TS
就报错了。
把类型设为某个值,就限制了变量在所有值中只能取指定的值。这称为类型字面量。
类型字面量:表示一个值的类型。
在TS
中可以将对象的属性设为可选属性。用法则是在对象的键和:
之间加上一个?
符号表示该属性是可选的。
let person: {
name: string,
age: number,
height?: number,
person = {
name: '小美',
age: 18
console.log(person)
person = {
name: '图图',
age: 18,
height: 175
console.log(person)
这里我们将height
设置成了可选的(类型为number | undefined
)。传不传都行。
当不确定一个对象的类型时或者在未来会对对象添加更多的键时,可以使用索引签名声明对象的类型。索引签名的语法为[key: T]: U
,意思是:这个对象里的键类型为T
,值则为U
类型。
let person: {
[key: string]: any
} = {
name: '牛爷爷',
age: 60,
person.height = 160
person.weight = 100
person.sex = '男'
有了索引签名,除了显式声明的键之外,还可以放心的添加更多键。要注意的是,键的类型只能是string
或number
类型。键的名称可以是任何的词。不一定像上面那样用key
。
类型别名用于给类型声明一个新名。
type Height = number
type Person = {
name: string,
height: Height
let person: Person = {
name: '图爸爸',
height: 180
类型别名和let
、const
变量声明一样,同一类型不能声明两次。并且也是采用块级作用域。每个代码块和每个函数都有自己的作用域,内部的类型别名会覆盖外部的类型别名。
type Name = '图图'
let n = Math.random() < 0.5
if (n) {
type Name = '小美'
let name: Name = '小美'
console.log('name=', name)
} else {
let name: Name = '图图'
console.log('name=', name)
交叉类型就是把多个类型合并在一起。并且具备多个类型的特性。也可以叫做并集类型。
type Bad = { name: string, isBad: boolean }
type Good = { name: string, isGood: boolean, clever: boolean }
type BadAndGood = Bad & Good
let person: BadAndGood = {
name: '蟑螂恶霸',
isBad: true,
isGood: false,
clever: false
上面的代码中,声明了两个不同的类型Bad
和Good
。然后使用&
运算符声明了Bad
和Good
两者之和的交叉类型BadAndGood
。该类型具备Bad
和Good
这两种类型的属性。
当一个变量存在不同的类型时,联合类型就派上用场了。
let height: number | string = 175
height = '180'
height = 190
height
的值的类型可以是string
类型,也可以是number
类型。
JS
和TS
函数声明方式一共有五种。
function Person1(name: string): string {
return `hello ${name}`
let Person2 = function (name: string): string {
return `hello ${name}`
let person3 = (name: string): string => {
return `hello ${name}`
let person4 = (name: string): string => `hello ${name}`
let person5 = new Function('name', 'return `hello ${name}`')
形参和实参
我们来简单回顾一下形参和实参。
形参:声明函数时指定的运行函数所需的数据。
实参:调用函数时传给函数的数据。
可选参数和默认参数
TS
的函数也是可以用?
将参数设为可选的。定义参数时最好是把必要的参数放在前,可选放在后。
function person(name: string, age?: number) {
return `大家好,我是${name}。今年${age || 18}岁`
console.log(person('图图', 20))
console.log(person('小美'))
JS
中的函数参数默认值,在TS
中一样支持的。把上面的函数改一下。
function person(name: string, age: number = 18) {
return `大家好,我是${name}。今年${age || 18}岁`
console.log(person('图图', 20))
console.log(person('小美'))
这个例子中,我们把可选参数age
改成了默认值。调用函数时,可以不传。
TS
中的剩余参数和ES6
中的剩余参数是一样的。以三点运算符(...
)表示。
function sum(...nums: number[]): number {
return nums.reduce((total, n) => total + n, 0)
console.log(sum(1, 2, 3))
注解this的类型
在TS
中,如果函数用到this
,就要在函数的第一个参数中声明this
的类型(放在其他参数之前),这样每次调用函数时,确保this
是你想要的类型。
function getDate(this: Date) {
return `${this.getFullYear()}-${this.getMonth()}-${this.getDate()}`
let day = getDate.call(new Date)
console.log(day)
想了解更多关于TS中的this
,可以到TS的官方文档查看。
在学习函数签名之前,先来给一个例子大家看:
function sum(a: number, b: number): number {
return a * b;
这个例子中的sum
是一个函数,它的类型是Function
。但有时候Function
类型并不是我们想要的。它并不能体现出函数的具体类型。
那么,sum
函数的类型要怎么表示呢?sum
是一个接受两个number
参数并返回一个number
的函数。在TS
中可以像下面这样来表示该函数的类型:
(a: number, b: number) => number
这个句法在TS
表示函数的类型,也叫函数签名(或叫类型签名)。它跟箭头函数非常相似。
函数签名只包含类型层面的代码。也就是说,只有类型没有值。因此,函数签名可以表示参数的类型、this
的类型、返回值的类型、剩余参数的类型和可选参数的类型。但无法表示默认值,因为默认值是值,而不是类型。函数签名没有函数的定义体,无法推导出返回类型,所以必须显式注解。
下面我们来看看函数签名的使用。
type Sum = (a: number, b: number) => number
let sum: Sum = (a, b = 30) => {
return a + b
console.log(sum(10, 20))
这个例子中,声明一个函数表达式sum
,并注解它的类型为Sum
。参数的类型不用再次注解,因为在定义Sum
类型时已经注解过了。给b
设置一个默认值。类型则从Sum
函数签名中推导出,但默认值是不知道的,因为Sum
是类型,不包含值。最后不许再次注解返回类型,在Sum
函数签名中已经声明为number
。
类型层面和值层面代码
类型层面的代码指的是只有类型和类型运算符的代码。其他都是值层面代码。看下面的例子。
function add(num: number): number | null {
if (num < 0) {
return null;
num++;
return num;
let num: number = 1;
let total = add(num);
if (total !== null) {
console.log(total);
这个例子中,函数的参数、返回值类型、联合类型运算符|
都是类型层面。
函数签名两种句法
函数签名句法有两种。上面用是简写型函数签名。还有一种是完整行函数签名。
type Sum = (a: number, b: number): number
type Sum = {
(a: number, b: number): number,
这两种写法完全相同,只是使用句法不同。对于比较复杂的函数时,推荐用完整型函数签名句法。也就是下面讲到的函数重载。
函数重载:具有多个函数签名的函数。
我们都知道在JS
中是没有函数重载的,但在TS
中有。下面我们通过函数签名来实现一个以不同的方式调用的函数。
type Sum = {
(a: number, b: number): number,
(a: number, b: number, c:number): number
let sum: Sum = (a:number, b:number, c?:number) => {
if (c !== undefined) {
return a + b +c
return a + b
console.log(sum(2, 3))
console.log(sum(2, 3, 5))
这个例子中,我们写两个函数签名:一个接受两个参数,另一个接受三个参数。根据传入的参数不同,函数体内所做的事情就不同。
声明函数重载还有另一种方式,我们来改造一下上面的例子。
function Sum(a: number, b: number): number
function Sum(a: number, b: number, c: number): number
function Sum(a: number, b: number, c?: number) {
if (c !== undefined) {
return a + b + c
return a + b
在类型层面施加约束的占位类型,也叫多态类型参数
了解泛型之前,我们先来看个例子。
function merge(arr1: string[], arr2: string[]): string[];
function merge(arr1: number[], arr2: number[]): number[];
function merge(arr1: object[], arr2: object[]): object[];
function merge(arr1: any, arr2: any) {
return arr1.concat(arr2)
const strings = merge(['a', 'b'], ['c', 'd', 'e'])
const numbers = merge([1, 2, 3], [4, 5])
const objs = merge([{ name: '图图' }], [{ name: '小美' }])
console.log(strings[0])
console.log(numbers[0])
console.log(objs[0].name)
这个例子中,简单的使用函数重载实现了一个合并字符串数组、数字数组、对象数组的merge
函数。访问strings
和numbers
数组的第一个元素都没问题。但是当访问objs
变量第一个元素的属性时,TS
抛出了错误。是因为object
无法描述对象的结构,所以抛出了错误。而且没有指明对象的具体结构。
为了解决这种问题,我们就要用到泛型了。把上面的函数接受的参数改写为泛型。如下:
function merge<T>(arr1: T[], arr2: T[]): T[];
function merge<T>(arr1: T[], arr2: T[]) {
return arr1.concat(arr2)
const strings = merge(['a', 'b'], ['c', 'd', 'e'])
const numbers = merge([1, 2, 3], [4, 5])
const objs = merge([{ name: '图图' }], [{ name: '小美' }])
console.log(strings[0])
console.log(numbers[0])
console.log(objs[0].name)
上面代码中,merge
函数使用一个泛型参数T
,但我们并不知道具体类型是什么。TS从传入的arr1
和arr2
中推导T
的类型。调用merge
函数时,TS推导出T
的具体类型之后,会把T
出现的每个地方都替换成推导出的类型。T
就像是一个占位类型,类型检查器会根据上下文填充具体的类型。
泛型使用尖括号<>
来声明(你可以把尖括号理解成type
关键字,只不过声明的是泛型)。尖括号的位置限定泛型的作用域(只有少数几个地方可以用尖括号),TS将确保当前作用域中相同的泛型参数最终都绑定同一个具体类型。鉴于上面的例子中括号的位置,TS将在调用merge
函数时为泛型T
绑定具体类型。而为T
绑定哪一个具体类型,就取决于调用merge
函数时传入的参数。
T
是一个类型名称,也可以使用任何名称,比如Name
、Person
、Value
等。
泛型还可以是多个,在尖括号里以逗号分隔开。
function getPerson<T, U>(name: T, age: U) {
return {
name, age
const person = getPerson("图图", 18);
console.log(person)
上面代码中,有两个泛型:表示人名的T
和年龄的U
,最后返回一个具备这两个值的对象。
声明泛型的位置不仅限制了泛型的作用域。还决定TS什么时候给泛型绑定具体类型。
type Person = {
<T, U>(name: T, age: U): {}
let person: Person = (x, y) => {
return {
<T, U>
在函数签名中声明,TS会在调用Person
类型的函数时为T
和U
绑定具体的类型。
如果把<T, U>
的作用域限制在函数签名Person
中,TS会要求在使用Perosn
时显示绑定类型。
type Person<T, U> = {
(name: T, age: U): {}
let person: Person = (name, age) => {
return { name, age }
type OtherPerson = Person
let person: Person<string, number> = (name, age) => {
return { name, age }
type OtherPerson = Person<string, number>
TS在使用泛型时会给泛型绑定具体类型,对于函数来说,在调用函数时。对于类,在实例化时。对于函数签名和接口,在使用别名和实现接口时。
以上的所有泛型例子,都是让TS自动推导出泛型。不过,也可以显示注解泛型。在显式注解泛型时。要么把所有的泛型都加上,要么都不注解。
function Person<T, U>(name: T, age: U): {} {
return { name, age };
console.log(Person('图图', 23))
console.log(Person<string, number>('小美', 18))
console.log(Person<string>('牛爷爷', 60))
console.log(Person<string, string>('图爸爸', 49))
用type
关键字可以给类型起新名字,泛型同样也可以。下面来定义一个Event
类型,用于描述DOM
事件。
type DomEvent<T> = {
target: T,
type: string
type DivEvent = DomEvent<HTMLDivElement | null>;
let myDiv: DivEvent = {
target: document.querySelector('#my-div'),
type: 'click'
let myDiv: DomEvent<HTMLDivElement | null> = {
target: document.querySelector('#my-div'),
type: 'click',
DomEvent
的target
属性指向触发事件的元素,比如一个div
或者button
等。要注意的是,在使用DomEvent
泛型时。必须显式注解类型参数,因为TS无法推导。能用泛型的地方,泛型别名一样生效。
泛型默认类型
函数的参数可以指定默认值,泛型参数也可以指定默认类型。
type DomEvent<T = HTMLElement> = {
target: T,
type: string
let myButton: DomEvent<HTMLButtonElement | null> = {
target: document.querySelector('#my-btn'),
type: 'click'
注意,泛型默认类型和函数可选参数一样的,有默认类型的泛型要放在没有默认类型的泛型后面。
type MyEvent<T = HTMLElement, Type> = {
target: T,
type: Type
let myDiv: MyEvent<HTMLElement | null, string> = {
target: document.querySelector('#my-div'),
type: 'scroll'
下面我们来看一个类的例子。
class Fruit {
fruitName: string
constructor(name: string) {
this.fruitName = name
getFruitName() {
return this.fruitName
let person = new Fruit('苹果');
这里声明了一个Fruit
类。该类有三个成员:一个fruitName
属性、一个构造函数和一个getFruitName
方法。引用类中的成员时用了this
,它表示我们访问类中的成员。
最后,使用new
运算符创建了Fruit
类的实例,它会调用之前给定的构造函数,创建一个Fruit
类型的新对象,并执行构造函数初始化它。
和JS一样,通过extends
关键字实现子类继承父类的属性和方法。
class Fruit {
fruitName: string
constructor(name: string) {
this.fruitName = name
getFruitName() {
return this.fruitName
class Grape extends Fruit {
fruitName: string
price: number
constructor(name: string, price: number) {
super(name);
this.fruitName = name
this.price = price
const grape = new Grape('葡萄', 15)
console.log(grape.getFruitName())
这个例子中,Grape
子类继承了Fruit
父类上的属性和方法。在创建Grape
的实例后,通过子类调用父类中的方法。
注意,子类有一个构造函数。在构造函数中必须调用super()
,把父子关系连接起来。并且要在构造函数访问this
的属性之前调用。
类中的修饰符
类中的属性和方法支持以下三个访问修饰符:
public
:公有的,任何地方都可以访问。
protected
:受保护的,只能在当前类及其子类中访问。
private
:私有的,只能在当前类访问。
public
在TS中,类的成员默认为公有的。你也可以把标记成员为public
。
class Fruit {
public fruitName: string
constructor(name: string) {
this.fruitName = name
public getFruitName() {
return this.fruitName
const fruit = new Fruit('西瓜')
console.log(fruit.getFruitName())
private
如果把成员标记为private
后,它只能在类中访问。
class Fruit {
public fruitName: string
private price: number
constructor(name: string, price: number) {
this.fruitName = name
this.price = price
public getFruitName() {
return this.fruitName
const fruit = new Fruit('猕猴桃', 50)
console.log(fruit.price)
protected
把成员标记为protected
后,在子类中可以访问,但不能在父类或子类外访问。
class Fruit {
public fruitName: string
protected price: number
constructor(name: string, price: number) {
this.fruitName = name
this.price = price
public getFruitName() {
return this.fruitName
class Watermelon extends Fruit {
constructor(name: string, price: number) {
super(name, price);
this.fruitName = name
this.price = price
public getPrice() {
return this.price
const watermelon = new Watermelon('西瓜', 3)
console.log(watermelon.getPrice())
console.log(watermelon.price)
readonly
声明实例属性时可以使用readonly
修饰符把属性标记为只读。readonly
修饰符不只是可以在类中使用,还可以把数组和对象属性设为只读。
class Fruit {
readonly fruitName: string
readonly price: number
constructor(name: string, price: number) {
this.fruitName = name
this.price = price
const fruit = new Fruit('火龙果', 10)
fruit.price = 20
fruit.fruitName = '荔枝'
抽象类是作为其它子类的父类使用,它不能被实例化。抽象类可以包含成员的实现细节。使用abstract
关键字来定义抽象类、抽象方法。
abstract class Fruit {
fruitName: string
price: number
constructor(name: string, price: number) {
this.fruitName = name
this.price = price
abstract getPrice(): number
abstract getName(): string
const fruit = new Fruit('龙眼', 15)
这个例子中,用abstract
关键字定义了Fruit
类,直接实例化Fruit
后,TS直接报错了。
下面我们通过子类去实现抽象类中定义的方法。
abstract class Fruit {
fruitName: string
price: number
constructor(name: string, price: number) {
this.fruitName = name
this.price = price
abstract getPrice(): number
abstract getName(): string
class Watermelon extends Fruit {
constructor(name: string, price: number) {
super(name, price)
this.fruitName = name
this.price = price
getPrice(): number {
return this.price
getName(): string {
return this.fruitName
const watermelon = new Watermelon('西瓜', 6)
console.log(watermelon.getPrice())
console.log(watermelon.getName())
这里的Watermelon
类继承了Fruit
抽象类,并且实现了Fruit
类上定义的getPrice
和getName
方法。如果忘记实现其中一个方法,TS就会抛出错误。
抽象类中的抽象方法是不会具体实现的,而是交给子类实现。抽象类必须要有一个抽象方法,继承抽象类的子类必须重写抽象方法。也就是说抽象类负责定义,子类负责实现。
和类型别名类似,接口是一种命名类型的方式。类型别名和接口算得上是同一个概念的两种句法,就跟函数表达式和函数声明之间的关系。但两者之间还是会存在一些差别的。先来看看二者的共同点:
type Fruits = {
name: string,
price: number,
feature: string,
weight: number,
interface Fruits {
name: string,
price: number,
feature: string,
weight: number,
在使用Fruits
类型别名的地方都能用Fruits
接口。两者都是定义结构。
还可以把类型组合在一起。
type Fruits = {
price: number,
type Watermelon = Fruits & {
feature: string,
type Banana = Fruits & {
weight: string,
interface Fruits {
price: number,
interface Watermelon extends Fruits {
feature: string,
interface Banana extends Fruits {
weight: string
接口不一定扩展其他接口。接口可以扩展任何结构:对象类型、类或其他接口。
接口和类型别名的差异
类型别名和接口之间有三种差别。
类型别名更通用,右边可以是任何类型,包括类型表达式(类型外加&
或|
运算符);而在接口声明中,右边只能是结构。看下面的例子。
type Str = string
type StrAndNum = Str | number
扩展接口时,TS会检查扩展的接口是否可赋值给被扩展接口。
interface Person {
good(x: number): string,
bad(x: number): string,
interface BadPerson extends Person {
good(x: number | string): number
bad(x: string): string
type Person = {
good(x: number): string,
bad(x: number): string,
type BadPerson = Person & {
good(x: number | string): number
bad(x: string): string
这个例子中,将接口换成类型别名,把extends
换成交集运算符&
,TS会把扩展和被扩展的类型组合在一起,最后的结果就是重载bad
的签名。
同一个作用域中的多个同名接口会自动合并;同一个作用域中的多个类型别名会导致编译时报错。这个特性叫做声明合并。
implements
(实现)关键字的作用是指明该类满足某个接口。和其他显示类型注解一样。这是给类添加类型层面约束的一种方式。这样做的目的是尽可能的保证类在实现上的正确性。看下面的例子。
interface Fruit {
getName(): string
setName(name: string): void
setPrice(price: number): void
class Banana implements Fruit {
name: string
price: number
constructor(name: string, price: number) {
this.name = name
this.price = price
getName(): string {
return this.name
setName(name: string): void {
this.name = name
setPrice(price: number): void {
this.price = price
const fruit = new Banana('香蕉', 5)
console.log(fruit.getName())
fruit.setPrice(10)
console.log(fruit.getPrice())
这个例子中,Banana
类必须实现Fruit
接口声明的所有方法。如果有需要,还可以在此基础上实现其他方法和属性。接口还可以声明实例属性,但不能带有可见性修饰符(private
、public
等等),也不能使用static
关键字。但是可以使用readonly
将实例属性标记为只读。
interface Fruit {
readonly name: string
getName(): string
setName(name: string): void
setPrice(price: number): void
一个类不限于只能实现一个接口,你想实现多少个都行。
interface Fruit {
name: string
price: number
getName(): string
interface FruitFeature {
getPrice(): number
class Banana implements Fruit, FruitFeature {
name: string
price: number
constructor(name: string, price: number) {
this.name = name
this.price = price
getName(): string {
return this.name
getPrice(): number {
return this.price
const fruit = new Banana('香蕉', 6)
console.log(fruit.getName())
console.log(fruit.getPrice())
如果忘记实现某个方法或属性,或者实现方式有问题,TS将会抛出错误。
类是结构化类型
和其他类型一样,TS会根据结构比较类,和类的名称没有关系。类跟其他类型是否兼容,要看结构。如果一个对象定义了同样的属性或者方法,也和类兼容。
class Person {
name: string
constructor(name: string) {
this.name = name
getName(): string {
return this.name
class OtherPerson {
name: string
constructor(name: string) {
this.name = name
getName(): string {
return this.name
function getPersonInfo(person: Person) {
return person.getName()
let person = new Person('图图')
let otherPerson = new OtherPerson('小美')
getPersonInfo(person)
getPersonInfo(otherPerson)
上面代码中,getPersonInfo
函数接收一个person
实例。当我们传入person
实例和otherPerson
实例时。TS并没有报错。在person
函数看来,这两个类是可互用的,这两个类都实现了getName
方法。如果把方法用到private
或protected
修饰符,情况就不一样了。
TypeScript采用的是结构化类型。对类来说,和一个类结构相同的类型是可以互相赋值的。
类型断言用作于手动指定一个值的类型。有两种句法。as
句法和尖括号<>
句法。下面来展示这两种句法的用法。
interface Fruit {
name: string,
price: number
let fruit: Fruit = {
name: '猕猴桃',
price: 30
function getFruitPrice(price: number): string | number {
return price
getFruitPrice(fruit.price as number)
上面的例子中,用类型断言as
告诉TS,price
是个数字,而不是string | number
类型。
在TypeScript中,通过id
获取一个DOM
节点,对该节点做某些操作时,通常都要判断该节点是否存在。尽管我们知道一定存在这个元素。但TypeScript只知道该节点的类型为Node | null
,所以得用if
语句判断。在不确定是否为null
的情况下,确实得这么做,但确定不可能是null | undefined
时,可以使用TypeScript的非空断言。
const dom = document.getElementById('item1')!;
dom.style.display = 'none';
const dom = document.getElementById('item1');
dom!.style.display = 'none';
使用非空断言运算符!
,告诉TypeScript,这个变量不可能为null | undefined
。
明确赋值断言
有这么一种情况,假设我们声明了一个变量userName
,但还没赋值就想对其做一些处理。实际上TS并不允许这么操作。但可以使用明确赋值断言告诉TS,userName
变量一定有值(注意下面的感叹号)。
let userName: string
userName.slice(0, 3)
let userName!: string
userName.slice(0, 3)
const
TS中有个特殊的const
类型,可以禁止变量的类型拓宽。const
类型用作类型断言。
let fruit = {
name: '苹果',
price: 10
} as const
fruit.price = 15
const
不仅可以限制类型拓宽,还会把成员设置成readonly
。不管数据结构嵌套有多深。
键入运算符
“键入” 运算符类似JavaScript对象查找字段的句法。用于查找对象属性的类型。用 “键入” 查找属性的类型时,只能使用方括号表示法,不能使用点号表示法。看下面的例子。
type APIResponse = {
status: string,
message: string,
result: {
userName: string,
userId: string,
state: number
type Result = APIResponse['result']
Result的类型为
userName: string;
userId: string;
state: number;
type Status = APIResponse['status']
keyof
keyof
运算符可以获取对象类型的所有键的类型,并返回一个字符串字面量的联合类型。
type Fruit = {
name: string
price: number,
weight: number
type FruitKeys = keyof Fruit
let price: FruitKeys = 'price'
let weight: FruitKeys = 'weight'
let fruitName: FruitKeys = 'Name'
有了keyof
运算符,搭配 “键入” 运算符可以安全的读取类型。下面我们来实现一个获取对象中指定属性的值函数。
function getProperty<O extends object, K extends keyof O>(obj: O, key: K): O[K] {
return obj[key]
let fruit: Fruit = {
name: '苹果',
price: 10,
weight: 1
console.log(getProperty(fruit, 'name'))
console.log(getProperty(fruit, 'price'))
console.log(getProperty(fruit, 'Weight'))
这个例子中,getProperty
函数接收一个对象obj
和一个键key
。keyof O
是一个字符串字面量的联合类型,表示obj
的所有键。K
类型是这个联合类型的子类型。比如,obj
的类型是{name: string, price: number, weight: number}
,那么keyof O
的类型就是name | price | weight
,而K
可以是类型'name'、'price'、'name'|'weight'
。接下来,O[K]
的类型就是在O
中查找K
得到的具体类型。如果K
是name
,那就返回一个字符串。如果K
是'price' | 'weight'
,就返回number
。
Record类型
Record
类型用于定义一个对象的键(key
)和值的类型(value
)。看下面的例子。
interface Options {
baseUrl: string;
env: string;
method: string;
let options: Record<keyof Options, string> = {
baseUrl: 'http://www.baidu.com',
env: 'dev',
method: 'post',
let options: Record<string, string> = {
baseUrl: 'http://www.baidu.com',
env: 'dev',
method: 'post',
这个例子中,Record
有两种用法。
第一种:使用Record
类型后,约束了options
对象的键必须和Options
接口的键一一对应,而值只能是string
。
第二种:只是要求options
对象的键为string
类型,并没有特意的限制options
对象的键。
映射类型是TS独有的语言特性。下面我们来看个简单的例子。
type Keys = 'name' | 'age' | 'height'
type Person = {
[P in Keys]: number | string
let person: Person = {
name: '小美',
age: 18,
height: 165
语法和索引签名的语法相同,只不过内部使用了for...in
遍历类型。字符串字面量联合类型Keys
,包含了要迭代的属性名集合,最后是结果类型number | string
。
其实,Record
类型也是用映射类型实现的:
type Record<K exteds keyof any, V> = {
[P in K]: V
下面来看几个例子,看看使用映射类型都能做哪些事:
type Result = {
userId: number
tags: string[],
personName: string
type APIResponse = {
message: string,
status: string,
result: Result
type OptionAPIResponse = {
[K in keyof APIResponse]?: APIResponse[K]
type NullAPIResponse = {
[K in keyof APIResponse]: APIResponse[K] | null
type ReadonlyAPIResponse = {
readonly [K in keyof APIResponse]: APIResponse[K]
type WritableAPIResponse = {
-readonly [K in keyof APIResponse]: APIResponse[K]
type MustAPIResponse = {
[K in keyof APIResponse]-?: APIResponse[K]
最后两个类型使用了减号-
和+
号运算符。一般情况下不会直接使用+
加号运算符,因为它通常蕴含在其他运算符中。在映射类型中,readonly
等价于+readonly
,?
等价于+?
。+
存在的意义只是为了确保整体协调。
内置映射类型
Record<Keys, Values>
上面已经提过Record
类型,用于指定对象的键和值类型。
Partial<Object>
Partial
类型用于把对象类型的每个字段都设置为可选的。
interface Person {
name: string;
age: number;
height: number;
let person: Partial<Person> = {
name: '图图',
height: 180,
Required<Object>
Required
类型用于把对象类型的每个字段都标记为必须的。
interface Person {
name?: string;
age?: number;
height?: number;
weight?: number | string;
let person: Required<Person> = {
name: '小美',
age: 18,
height: 170,
Readonly<Object>
Readonly
类型用于把对象类型中的每个字段都设置为只读。
interface ListItem {
id: number;
area: string;
goodsName: string;
price: number;
let goodsInfo: Readonly<ListItem> = {
id: 1,
area: '深圳市南山',
goodsName: '罗技MX3 master3',
price: 499,
goodsInfo.price = 899;
Pick<O, K>
Pick
类型用于从一个对象类型中,选取指定的属性,并返回一个新定义的类型。
interface APIResponse {
message: string,
status: string,
result: string[]
type Result = Pick<APIResponse, 'result'>
let res1: Result = {
result: ['1', '2', '3']
}
let res2: Result = {
result: ['1', '2', '3'],
status: 'C0000'
interface ArrayResult {
notes: string[];
用Pick
类型新建了一个Result
类型,和APIResponse
建立映射。在使用Result
类型时,res2
对象中多出了一个status
键。TS报错了。因为Result
的类型为{ result: string[] }
。
条件类型是TypeScript中比较独特的特性,语法和JavaScript的三元表达式差不多。只不过是用在类型中。一起来看下面的例子。
type IsString<T> = T extends string ? true : false
type X = IsString<string>
type Y = IsString<number>
这里声明了一个类型IsString
,它有一个泛型参数T
。条件类型为T extends string
,也就是说T
是不是为number
类型或子类型。如果是,那么得到的类型就为true
。否则就是false
。
条件类型还可以进行嵌套,而且不限于用在类型别名当中,用到类型的地方几乎都可以用。
infer关键字
infer
关键字表示在条件类型中待推导的类型。下面来看个例子。
type ElementType<T> = T extends (infer U)[] ? U : T
type Y = ElementType<number[]>
let y: Y = 1
type ElementType2<T> = T extends unknown[] ? T[number] : T
type X = ElementType2<number[]>
在这个例子中,ElementType
类型接收泛型参数T
。注意,infer
子句声明了一个新的类型U
,TS会根据传给ElementType
的T
推导出U
的类型。U
是在行内声明的,没有和T
一起。
下面我们再来个复杂一点的例子。
type Person<T> = T extends { name: infer P; age: infer P } ? P : T
type Age = Person<{ name: string, age: number }>
type Name = Person<{ name: string, age: string }>
let personName: Name = '小美'
let age: Age = 18
let weight: Name = 50
可以看到,Age
的类型为string | number
,Name
类型为string
。利用这一特性,可以将元组中的类型转成联合类型。
内置条件类型
Exclude<T, U>
Exclude
用于计算在T
中而不在U
中的类型。
type X = number | string
type Name = string
type Y = Exclude<X, Name>
Extract<T, U>
Extract
用于计算T
中可赋值给U
的类型。
type X = number | boolean
type Y = string | boolean
type Total = Extract<X, Y>
NonNullable<T>
NonNullable
用于从T
中排除null
和undefined
。
type X = null | string | undefined | boolean
type Y = NonNullable<X>
ReturnType<F>
ReturnType
用于计算函数的返回类型(不适用于泛型和重载的函数)。
type CallBack = (p: Record<string, unknown>) => string | null
type RType = ReturnType<CallBack>
InstanceType<C>
InstanceType
用于计算类构造方法的实例类型。
type X = { new(): Y }
type Y = { name: string }
type I = InstanceType<X>
在开发的过程中,难以避免出现多个同名但含义不同的函数或者变量等等。虽说不建议这样,但总难以避免这些问题发生。而命名空间就能解决你的这些烦恼。
假设有这么两个文件:一个文件是封装请求的模块,另一个是使用该模块发起请求。
exprot namespace Request {
export function get(url: string): Promise<unknown> {
return new Promise((resolve) => {
resolve(`请求url为${url}`)
import { Request } from './request'
async function getList() {
const res = await Request.get('http://baidu.com')
console.log(res)
getList()
namespace
关键词表示命名空间,命名空间必须要有名称。可以导出函数、变量、类型、接口或其他命名空间。如果namespace
块没有显式导出代码,就表示为所在块的私有代码。
命名空间还可以导出命名空间,因此命名空间可以进行嵌套。比如Request
模块增加了其他的请求方法,分成几个子模块。可以改写成命名空间。
export namespace Request {
export namespace Get {
export function get(url: string): Promise<unknown> {
return new Promise((resolve) => {
resolve(`请求url为${url}`)
export namespace Post {
export function post(url: string, data: Record<string, unknown>): Promise<unknown> {
return new Promise((resolve) => {
resolve(`请求url为${url}`)
import { Request } from './get'
async function getList() {
const res = await Request.Get.get('http://baidu.com')
console.log(res)
async function saveForm() {
const data = {
userName: '小美',
password: '123456'
const res = await Request.Post.post('http://baidu.com', data)
getList()
saveForm()
现在我们把Request
模块中的请求方法分成了几个子命名空间。不过,命名空间和接口一样。可以声明多个同名的命名空间,TS会递归合并名称相同的命名空间。
export namespace Request {
export namespace Get {
export function get(url: string): Promise<unknown> {
return new Promise((resolve) => {
resolve(`请求url为${url}`)
export namespace Request {
export namespace Post {
export function post(url: string, data: Record<string, unknown>): Promise<unknown> {
return new Promise((resolve) => {
resolve(`请求url为${url}`)
用TypeScript开发的过程中。都会新建一个types
文件,里面的文件扩展名为.d.ts
。如果类型声明不多的话,在顶级目录建一个types.d.ts
的文件。这正是类型声明文件。类型声明的语法和常规的TS代码差不多。不过,还是有一些区别的。
类型声明只含类型,不能有值。这说明,类型声明不能实现函数、类、对象或变量,参数也不能有默认值。
类型声明虽然不能定义值,但可以声明JS代码中定义了某个值。不过,得用declare
关键字。
类型声明只声明使用方可见的类型。如果代码不导出,或者是函数体内的局部变量,则不为其声明类型。
类型声明可以做到以下几件事:
告诉TypeScript,JavaScript文件定义了某个全局变量。
定义在项目中用到的类型。
描述通过npm安装的第三方模块。
类型声明按照约定,如果有对应的.js
文件,类型声明文件使用.d.ts
扩展名;否则,使用.ts
扩展名。
外参变量声明让TypeScript知道全局变量的存在,不用显式导入即可在项目任何.ts
、.d.ts
文件里使用。
例如,最近在用vue3
和TypeScript做项目时,用到了process
对象设置axios
的baseURL
。编辑器提示报错process
没定义,但代码在浏览器上运行也没有报错。经过百度才知道,vue3中已经将process
移除了。要在vite.config.ts
里面单独定义,并且要在types
文件夹下添加一个process
类型声明的文件。告诉TS,有一个全局对象process
。编辑器的报错提示也就消失了。
declare const process: {
env: {
baseURL: string;
外参变量声明不止是可以声明变量,还可以声明方法declare function
、declare class
等等。
外参类型声明通常用于定义数据类型。例如,后端返回的数据中有哪些字段,这些字段的类型又是什么。
export interface Response {
status: string;
message: string;
result: ResponseListResult | ResponseObjectResult;
interface ResponseListResult {
items: [];
pageCount: number;
currentPage: number;
recordCount: number;
interface ResponseObjectResult {
data: Record<string, never>;
async function requestData() {
try {
const response: Response = await getList();
} catch (error) {
console.log(error);
这样有很大的好处,在输入代码的过程中,支持TypeScript的编辑器(例如VSCode)会智能提示定义的字段。
在使用一些第三方库时,这些库大多数都是用JS写的,并且也没有声明文件。当引入时,会提示找不到模块的声明文件。有两种解决方案,一是安装该库的声明文件(VSCode会提示你安装)。二是我们自己编写一个该库的声明文件。看下面的例子。
import TIM from 'tim-js-sdk'
Error Could not find a declaration file for module 'tim-js-sdk'.
'ts/node_modules/tim-js-sdk/tim-js.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/tim-js-sdk` if it exists or add a new declaration (.d.ts)
file containing `declare module 'tim-js-sdk';`
引入tim-js-sdk
后,提示找不到模块tim-js-sdk
的声明文件。接下来,我们为它创建一个全局声明文件。
import TIM from 'tim-js-sdk'
declare module 'tim-js-sdk' {
export default Object
这里只是为了做演示,给tim-js-sdk
定义导出一个Object
。此时报错就消失了。实际上这个Object
类型定不定义都无所谓,直接声明一个模块也是可以的。只是说这个模块的类型是一个any
。像下面这样:
import TIM from 'tim-js-sdk'
declare module 'tim-js-sdk'
模块声明支持通配符导入。有了通配符导入,可以给任何导入的路径声明类型。路径使用通配符*
匹配即可。
declare module '*.png';
declare module 'json!*' {
let value: object;
export default value;
declare module '*.css' {
let css: CSSRuleList;
export default css;
现在我们就可以引入匹配*.png
、json!*
、*.css
文件内容了。
import logo from '../assets/logo.png';
import jsonFile from './json!File';
import cssFile from './index.css';
cssFile;
三斜杠指令
三斜杠指令以三条斜线///
开头,三斜杠指令只能放在包含它的文件最上面。后面跟一个可用的XML标签;各XML标签有一些必须设置的属性。
types指令
types
指令用于对某个包依赖。
声明依赖@types/node/index.d.ts
。
只有在你需要写一个d.ts
文件时才用这个指令。
path指令
path
指令和types
指令类似,用于本地声明文件之间的依赖。
以上是这几个月学习TypeScript过程所做的总结。哪里有不对的地方,请各位大佬多多指点!如果文章对你有所帮助,欢迎点赞加关注哦!
《TypeScript编程》
TypeScript官网
图图学编程
前端工程师 @ 深圳某中型公司
16.6k
粉丝