目录
- 类型别名
- 字符串字面量类型
- 元组
- 枚举
- 类
- 类的概念
- TypeScript 中类的用法
- 参数属性
- readonly
- 抽象类
- 类的类型
- 类与接口
- 泛型
- 泛型类
- 泛型参数的默认类型
- 声明合并
- 函数的合并
- 接口的合并
本文讲述了typescript开发的一些高级的类型与技术,算是对于基础知识点的补充,具体内容包括:比如元组、枚举类、接口、泛型相关概念等。虽说是进阶,但是内容不算多也并不难理解。
类型别名
类型别名用来给一个类型起个新名字。
type Name = string; | |
type NameResolver = () => string; | |
type NameOrResolver = Name | NameResolver; | |
function getName(n: NameOrResolver): Name { | |
if (typeof n === 'string') { | |
return n; | |
} else { | |
return n(); | |
} | |
} |
上例中,我们使用 type 创建类型别名。
类型别名常用于联合类型。
字符串字面量类型
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
type EventNames = 'click' | 'scroll' | 'mousemove'; | |
function handleEvent(ele: Element, event: EventNames) { | |
// do something | |
} | |
handleEvent(document.getElementById('hello'), 'scroll'); // 没问题 | |
handleEvent(document.getElementById('world'), 'dblclick'); // 报错,event 不能为 'dblclick' | |
// index.ts(,47): error TS2345: Argument of type '"dblclick"' is not assignable to parameter of type 'EventNames'. |
上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。
注意,类型别名与字符串字面量类型都是使用 type 进行定义。
元组
数组合并了相同类型的对象,而元组(Tuple)合并了不同类型的对象。
元组起源于函数编程语言(如 F#),这些语言中会频繁使用元组。
定义一对值分别为 string 和 number 的元组:
let tom: [string, number] = ['Tom',];
当赋值或访问一个已知索引的元素时,会得到正确的类型:
let tom: [string, number]; | |
tom[] = 'Tom'; | |
tom[] = 25; | |
tom[].slice(1); | |
tom[].toFixed(2); |
也可以只赋值其中一项:
let tom: [string, number]; | |
tom[] = 'Tom'; |
但是当直接对元组类型的变量进行初始化或者赋值的时候,需要提供所有元组类型中指定的项。
let tom: [string, number]; | |
tom = ['Tom',]; | |
let tom: [string, number]; | |
tom = ['Tom']; | |
// Property '' is missing in type '[string]' but required in type '[string, number]'. |
越界的元素
当添加越界的元素时,它的类型会被限制为元组中每个类型的联合类型:
let tom: [string, number]; | |
tom = ['Tom',]; | |
tom.push('male'); | |
tom.push(true); | |
// Argument of type 'true' is not assignable to parameter of type 'string | number'. |
枚举
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。
枚举使用 enum 关键字来定义:
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
枚举成员会被赋值为从 0 开始递增的数字,同时也会对枚举值到枚举名进行反向映射:
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; | |
console.log(Days["Sun"] ===); // true | |
console.log(Days["Mon"] ===); // true | |
console.log(Days["Tue"] ===); // true | |
console.log(Days["Sat"] ===); // true | |
console.log(Days[] === "Sun"); // true | |
console.log(Days[] === "Mon"); // true | |
console.log(Days[] === "Tue"); // true | |
console.log(Days[] === "Sat"); // true |
事实上,上面的例子会被编译为:
var Days; | |
(function (Days) { | |
Days[Days["Sun"] =] = "Sun"; | |
Days[Days["Mon"] =] = "Mon"; | |
Days[Days["Tue"] =] = "Tue"; | |
Days[Days["Wed"] =] = "Wed"; | |
Days[Days["Thu"] =] = "Thu"; | |
Days[Days["Fri"] =] = "Fri"; | |
Days[Days["Sat"] =] = "Sat"; | |
})(Days || (Days = {})); |
手动赋值
我们也可以给枚举项手动赋值:
enum Days {Sun =, Mon = 1, Tue, Wed, Thu, Fri, Sat}; | |
console.log(Days["Sun"] ===); // true | |
console.log(Days["Mon"] ===); // true | |
console.log(Days["Tue"] ===); // true | |
console.log(Days["Sat"] ===); // true |
上面的例子中,未手动赋值的枚举项会接着上一个枚举项递增。
类
传统方法中,JavaScript 通过构造函数实现类的概念,通过原型链实现继承。而在 ES6 中,我们终于迎来了 class。
TypeScript 除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。
类的概念
虽然 JavaScript 中有类的概念,但是可能大多数 JavaScript 程序员并不是非常熟悉类,这里对类相关的概念做一个简单的介绍。
- 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
- 对象(Object):类的实例,通过 new 生成
- 面向对象(OOP)的三大特性:封装、继承、多态
- 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
- 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
- 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat
- 存取器(getter & setter):用以改变属性的读取和赋值行为
- 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
- 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
- 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口
TypeScript 中类的用法
public private 和 protected
TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected。
- public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
- private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
- protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
下面举一些例子:
class Animal { | |
public name; | |
public constructor(name) { | |
this.name = name; | |
} | |
} | |
let a = new Animal('Jack'); | |
console.log(a.name); // Jack | |
a.name = 'Tom'; | |
console.log(a.name); // Tom |
参数属性
修饰符和readonly还可以使用在构造函数参数中,等同于类中定义该属性同时给该属性赋值,使代码更简洁。
class Animal { | |
// public name: string; | |
public constructor(public name) { | |
// this.name = name; | |
} | |
} |
readonly
只读属性关键字,只允许出现在属性声明或索引签名或构造函数中。
class Animal { | |
readonly name; | |
public constructor(name) { | |
this.name = name; | |
} | |
} | |
let a = new Animal('Jack'); | |
console.log(a.name); // Jack | |
a.name = 'Tom'; | |
// index.ts(,3): TS2540: Cannot assign to 'name' because it is a read-only property. |
注意如果 readonly 和其他访问修饰符同时存在的话,需要写在其后面。
class Animal { | |
// public readonly name; | |
public constructor(public readonly name) { | |
// this.name = name; | |
} | |
} |
抽象类
abstract 用于定义抽象类和其中的抽象方法。
abstract class Animal { | |
public name; | |
public constructor(name) { | |
this.name = name; | |
} | |
public abstract sayHi(); | |
} | |
class Cat extends Animal { | |
public sayHi() { | |
console.log(`Meow, My name is ${this.name}`); | |
} | |
} | |
let cat = new Cat('Tom'); |
上面的例子中,我们实现了抽象方法 sayHi,编译通过了。
需要注意的是,即使是抽象方法,TypeScript 的编译结果中,仍然会存在这个类,上面的代码的编译结果是:
var __extends = | |
(this && this.__extends) || | |
function (d, b) { | |
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; | |
function __() { | |
this.constructor = d; | |
} | |
d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __()); | |
}; | |
var Animal = (function () { | |
function Animal(name) { | |
this.name = name; | |
} | |
return Animal; | |
})(); | |
var Cat = (function (_super) { | |
__extends(Cat, _super); | |
function Cat() { | |
_super.apply(this, arguments); | |
} | |
Cat.prototype.sayHi = function () { | |
console.log('Meow, My name is ' + this.name); | |
}; | |
return Cat; | |
})(Animal); | |
var cat = new Cat('Tom'); |
类的类型
给类加上 TypeScript 的类型很简单,与接口类似:
class Animal { | |
name: string; | |
constructor(name: string) { | |
this.name = name; | |
} | |
sayHi(): string { | |
return `My name is ${this.name}`; | |
} | |
} | |
let a: Animal = new Animal('Jack'); | |
console.log(a.sayHi()); // My name is Jack |
类与接口
实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。
举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:
interface Alarm { | |
alert(): void; | |
} | |
class Door { | |
} | |
class SecurityDoor extends Door implements Alarm { | |
alert() { | |
console.log('SecurityDoor alert'); | |
} | |
} | |
class Car implements Alarm { | |
alert() { | |
console.log('Car alert'); | |
} | |
} |
一个类可以实现多个接口:
interface Alarm { | |
alert(): void; | |
} | |
interface Light { | |
lightOn(): void; | |
lightOff(): void; | |
} | |
class Car implements Alarm, Light { | |
alert() { | |
console.log('Car alert'); | |
} | |
lightOn() { | |
console.log('Car light on'); | |
} | |
lightOff() { | |
console.log('Car light off'); | |
} | |
} |
上例中,Car 实现了 Alarm 和 Light 接口,既能报警,也能开关车灯。
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:
function createArray(length: number, value: any): Array<any> { | |
let result = []; | |
for (let i =; i < length; i++) { | |
result[i] = value; | |
} | |
return result; | |
} | |
createArray(, 'x'); // ['x', 'x', 'x'] |
这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型:
Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。
这时候,泛型就派上用场了:
function createArray<T>(length: number, value: T): Array<T> { | |
let result: T[] = []; | |
for (let i =; i < length; i++) { | |
result[i] = value; | |
} | |
return result; | |
} | |
createArray<string>(, 'x'); // ['x', 'x', 'x'] |
上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。
泛型类
与泛型接口类似,泛型也可以用于类的类型定义中:
class GenericNumber<T> { | |
zeroValue: T; | |
add: (x: T, y: T) => T; | |
} | |
let myGenericNumber = new GenericNumber<number>(); | |
myGenericNumber.zeroValue =; | |
myGenericNumber.add = function(x, y) { return x + y; }; |
泛型参数的默认类型
在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。
function createArray<T = string>(length: number, value: T): Array<T> { | |
let result: T[] = []; | |
for (let i =; i < length; i++) { | |
result[i] = value; | |
} | |
return result; | |
} |
声明合并
如果定义了两个相同名字的函数、接口或类,那么它们会合并成一个类型:
函数的合并
我们可以使用重载定义多个函数类型:
function reverse(x: number): number; | |
function reverse(x: string): string; | |
function reverse(x: number | string): number | string { | |
if (typeof x === 'number') { | |
return Number(x.toString().split('').reverse().join('')); | |
} else if (typeof x === 'string') { | |
return x.split('').reverse().join(''); | |
} | |
} |
接口的合并
接口中的属性在合并时会简单的合并到一个接口中:
interface Alarm { | |
price: number; | |
} | |
interface Alarm { | |
weight: number; | |
} |
相当于:
interface Alarm { | |
price: number; | |
weight: number; | |
} |