本文为系列文章《TypeScript 简明教程》中的一篇。
接口
TypeScript 中,我们使用接口来描述对象或类的具体结构。接口的概念在 TypeScript 中是至关重要的。它就像是你与程序签订的一个契约,定义一个接口就意味着你答应程序:未来的某个值(或者类)一定会符合契约中所规定的模样,如果不符合,TS 就会直接在编译时报错。
感兴趣的同学可以了解一下 鸭子类型。
举个例子:
interface Phone {
model: string
price: number
}
let newPhone: Phone = {
model: 'iPhone XS',
price: 8599,
}
复制代码
上面的例子中,我们定义了一个接口 Phone
,它约定:任何类型为 Phone
的值,有且只能有两个属性:string
类型的 model
属性以及 number
类型的 price
属性。之后,我们声明了一个变量 newPhone
,它的类型为 Phone
,而且遵照契约,将 model
赋值为字符串,price
赋值为数值。
接口一般首字母大写。在某些编程语言会建议使用
I
作为前缀。关于是否要使用I
前缀,tslint 有一条 专门的规则,请根据团队编码风格自行选择。
多一些属性和少一些属性都是不允许的。
let phoneA: Phone = {
model: 'iPhone XS',
} // Error: Property 'price' is missing in type '{ model: string; }' but required in type 'Phone'
let phoneB: Phone = {
model: 'iPhone XS',
price: 8599,
producer: 'Apple',
} // Error: Property 'producer' doesn't exist on type `Phone`.
复制代码
接口作为类型注解,只在编译时起作用,不会出现在最终输出的 JS 代码中。
可选属性
对于某个可能存在的属性,我们可以在该属性后加上 ?标记
表示这个属性是可选的。
interface Phone {
model: string
price: number
producer?: string
}
let newPhone: Phone = {
model: 'iPhone XS',
price: 8599, // OK
}
let phoneB: Phone = {
model: 'iPhone XS',
price: 8599,
producer: 'Apple', // OK
}
复制代码
任意属性
某些情况下,我们可能只知道接口中部分属性及它们的类型。或者,我们希望能够在初始化后动态添加对象的属性。这时,我们可以使用下面这种写法。
interface Phone {
model: string
price: number
producer?: string
[propName: string]: any
}
let phoneB: Phone = {
model: 'iPhone XS',
price: 8599,
}
phoneB.storage = '256GB' // OK
复制代码
上面,我们定义任意属性的签名为 string
类型,值为 any
类型。注意:任意属性的值类型必须包含所有已知属性的值类型。 上述例子中,any
包括 string
和 number
类型。
只读属性
接口中,我们可以使用 readonly
标记某个属性是只读的。当我们试图修改它时,TS 会提示报错。
interface Phone {
readonly model: string
price: number
}
let phoneA: Phone = {
model: 'iPhone XS',
price: 8599,
}
phoneA.model = 'iPhone Air' // Error: Cannot assign to 'model' because it is a read-only property.
复制代码
函数
呼,终于说到函数了。JavaScript 中有两种定义函数的方法。
// 命名函数
function add(x, y) {
return x + y
}
// 匿名函数
const add = function(x, y) { return x + y }
复制代码
对于这两种方法,添加类型注释的方式大同小异。
// 命名函数
function add(x: number, y: number): number {
return x + y
}
// 匿名函数
const add = function(x: number, y: number): number {
return x + y
}
复制代码
上面我们定义了 add
函数,它接受两个 number
类型的参数,并规定其返回值为 number
类型。
调用函数时,传入参数的类型和数量必须与定义时保持一致。
add(1, 2) // OK
add('1', 0) // Error
add(1, 2, 3) // Error
复制代码
可选参数
使用 ?标记
可以标识某个参数是可选的。可选参数必须放在必要参数后面。
function increment(x: number, step?: number): number {
return x + (step || 1)
}
increment(10) // => 11
复制代码
参数默认值
ES6 允许我们为参数添加默认值。作为 JS 的超集,TS 自然也是支持参数默认值的。
function increment(x: number, step: number = 1): number {
return x + step
}
increment(10) // => 11
复制代码
因为具有参数默认值的参数必然是可选参数,所以无需再使用 ?
标记该参数时可选的。
这里,step: number = 1
可以简写为 step = 1
,TS 会根据类型推断自动推断出 step
应为 number
类型。
与可选参数不同的是,具有默认值的参数不必放在必要参数后面。下面的写法也是允许的,只是在调用时,必须明确地传入 undefined
来获取默认值。
function increment(step = 1, x: number): number {
return x + step
}
increment(undefined, 10) // => 11
复制代码
剩余参数
ES6 允许我们使用剩余参数将一个不定数量的参数表示为一个数组。TypeScript 中我们可以这样写。
function sum(...args: number[]): number {
return args.reduce((prev, cur) => prev + cur)
}
sum(1, 2, 3) // => 6
复制代码
注意与 arguments
对象进行 区分。
接口中的方法
对于接口中的方法,我们可以使用如下方式去定义:
interface Animal {
say(text: string): void
}
// 或者
interface Animal {
say: (text: string) => void
}
复制代码
这两种注解方法的效果是一致的。
函数重载
函数重载允许你针对不同的参数进行不同的处理,进而返回不同的数据。
因为 JavaScript 在语言层面并不支持重载,我们必须在函数体内自行判断参数进行针对性处理,从而模拟出函数重载。
function margin(all: number);
function margin(vertical: number, horizontal: number);
function margin(top: number, right: number, bottom: number, left: number);
function margin(a: number, b?: number, c?: number, d?: number) {
if (b === undefined && c === undefined && d === undefined) {
b = c = d = a
} else if (c === undefined && d === undefined) {
c = a
d = b
}
return {
top: a,
right: b,
bottom: c,
left: d,
}
}
console.log(margin(10))
// => { top: 10, right: 10, bottom: 10, left: 10 }
console.log(margin(10, 20))
// => { top: 10, right: 20, bottom: 10, left: 20 }
console.log(margin(10, 20, 20, 20))
// => { top: 10, right: 20, bottom: 20, left: 20 }
console.log(margin(10, 20, 20))
// Error
复制代码
上述例子中,前面三个声明了三种函数定义,编译器会根据这个顺序来处理函数调用,最后一个为最终的函数实现。需要注意的是,最后的函数实现参数类型必须包含之前所有的参数类型定义。因此,在定义重载的时候,一定要把最精确的定义放在最前面。
类
以前,JavaScript 中并没有类的概念,我们使用原型来模拟类的继承,直到 ES6 的出现,引入了 class
关键字。如果你对 ES6 的 class
还不是很了解,建议阅读 ECMAScript 6 入门 - Class。
TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。
访问修饰符
TypeScript 中可以使用三种修饰符:public
、private
、protected
。
public
修饰符
表示属性或方法是公有的,在类内部、子类内部、类的实例中都能被访问。默认情况下,所有属性和方法都是 public
的。
class Animal {
public name: string
constructor(name) {
this.name = name
}
}
let cat = new Animal('Tom')
console.log(cat.name); // => Tom
复制代码
private
修饰符
表示属性或方法是私有的,只能在类内部访问。
class Animal {
private name: string
constructor(name) {
this.name = name
}
greet() {
return `Hello, my name is ${ this.name }.`
}
}
let cat = new Animal('Tom')
console.log(cat.name); // Error: 属性“name”为私有属性,只能在类“Animal”中访问。
console.log(cat.greet()) // => Hello, my name is Tom.
复制代码
protected
修饰符
表示属性或方法是受保护的,与 private
近似,不过被 protected
修饰的属性或方法也能被其子类访问。
class Animal {
protected name: string
constructor(name) {
this.name = name
}
}
class Cat extends Animal {
constructor(name) {
super(name)
}
greet() {
return `Hello, I'm ${ this.name } the cat.`
}
}
let cat = new Cat('Tom')
console.log(cat.name); // Error: 属性“name”受保护,只能在类“Animal”及其子类中访问。
console.log(cat.greet()) // => Hello, I'm Tom the cat.
复制代码
注意,TypeScript 只做编译时检查,当你试图在类外部访问被 private
或者 protected
修饰的属性或方法时,TS 会报错,但是它并不能阻止你访问这些属性或方法。
目前有一个提案,建议在语言层面使用
#
前缀标记某个属性或方法为私有的,感兴趣的可以看 这里。
抽象类
抽象类是某个类具体实现的抽象表述,作为其他类的基类使用。
它具有两个特点:
- 不能被实例化
- 其抽象方法必须被子类实现
TypeScript 中使用 abstract
关键字表示抽象类以及其内部的抽象方法。
继续使用上面的 Animal
类的例子:
abstract class Animal {
public abstract makeSound(): void
public move() {
console.log('Roaming...')
}
}
class Cat extends Animal {
makeSound() {
console.log('Meow~')
}
}
let tom = new Cat()
tom.makeSound() // => 'Meow~'
tom.move() // => 'Roaming...'
复制代码
上述例子中,我们创建了一个抽象类 Animal
,它定义了一个抽象方法 makeSound
。然后,我们定义了一个 Cat
类,继承自 Animal
。因为 Animal
定义了 makeSound
抽象类,所以我们必须在 Cat
类里面实现它。不然的话,TS 会报错。
// Error: 非抽象类“Cat”没有实现继承自“Animal”类的抽象成员“makeSound”。
class Cat extends Animal {
meow() {
console.log('Meow~')
}
}
复制代码
类与接口
类可以实现(implement)接口。通过接口,你可以强制地指明类遵守某个契约。你可以在接口中声明一个方法,然后要求类去具体实现它。
interface ClockInterface {
currentTime: Date
setTime(d: Date)
}
class Clock implements ClockInterface {
currentTime: Date
setTime(d: Date) {
this.currentTime = d
}
}
复制代码
接口与抽象类的区别
- 类可以实现(
implement
)多个接口,但只能扩展(extends
)自一个抽象类。 - 抽象类中可以包含具体实现,接口不能。
- 抽象类在运行时是可见的,可以通过
instanceof
判断。接口则只在编译时起作用。 - 接口只能描述类的公共(
public
)部分,不会检查私有成员,而抽象类没有这样的限制。
小结
本篇主要介绍了 TypeScript 中的几个重要概念:接口、函数和类,知道了如何用接口去描述对象的结构,如何去描述函数的类型以及 TypeScript 中类的用法。