变量

联合类型声明

为变量指定多个可能的类型:

  1. let a : number | string

在解构中的应用

  1. let o = { a: 'Hello', b: 2019 }
  2. let { a: c, b: d }: { a: string, b: number } = o;

编译成JS:

  1. var o = { a: 'Hello', b: 2019}
  2. var c = o.a, d = o.b;

同以下JS语法:

  1. let { a: c, b: d } = o;

其实也就是将 o 解构的同时, 重新将 ab 命名为 cd

函数

在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):

  1. // 函数声明(Function Declaration)
  2. function sum(x, y) {
  3. return x + y;
  4. }
  5. // 函数表达式(Function Expression)
  6. let mySum = function (x, y) {
  7. return x + y;
  8. };

下面说说在TS中如何为函数参数和返回值添加类型约束。

定义函数的几种方式

一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:

  1. // 命名函数
  2. function myAdd(x: number, y: number): number {
  3. return x + y;
  4. }
  5. // 匿名函数
  6. let myAdd = function(x: number, y: number): number { return x + y; };
  7. // 箭头函数
  8. let myAdd = (x: number, y: number): number => x + y
  9. // 完整的声明
  10. let myAdd: (x: number, y: number) => number =
  11. function(x: number, y: number): number { return x + y; };

用接口定义函数的形状

我们也可以使用接口的方式来定义一个函数需要符合的形状:

  1. interface SearchFunc {
  2. (source: string, subString: string): boolean;
  3. }
  4. let mySearch: SearchFunc = function(source: string, subString: string) {
  5. return source.search(subString) !== -1;
  6. }

可选参数

  1. function func(a: string, b?: string) {
  2. return a + b
  3. }
  4. func('Hello')
  5. func('Hello', 'world')
  6. func() // Error: Expected 1-2 arguments, but got 0.

需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必须参数了。

默认参数

  1. function func(a: string = 'Hi', b?: string) {
  2. return a + b
  3. }
  4. func() // okay
  5. func('Hello') // okay
  6. func('Hello', 'world') // okay

如果给一个参数设置了默认值,则也不需要显性传递参数。

但是如果第二个参数不是可选的, 则仍然报错:

  1. function func(a: string = 'Hi', b: string) {
  2. return a + b
  3. }
  4. console.log(func('Hello', 'world'))
  5. console.log(func()) // Error: Expected 2 arguments, but got 0.
  6. console.log(func('Hello')) // Error: Expected 2 arguments, but got 1.

剩余参数

可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数)

  1. function buildName(firstName: string, ...restOfName: string[]) {
  2. return firstName + " " + restOfName.join(" ");
  3. }
  4. let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

函数重载

重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。

比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'

利用联合类型,我们可以这么实现:

  1. function reverse(x: number | string): number | string {
  2. if (typeof x === 'number') {
  3. return Number(x.toString().split('').reverse().join(''));
  4. } else if (typeof x === 'string') {
  5. return x.split('').reverse().join('');
  6. }
  7. }

然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。

这时,我们可以使用重载定义多个 reverse 的函数类型:

  1. function reverse(x: number): number;
  2. function reverse(x: string): string;
  3. function reverse(x: number | string): number | string {
  4. if (typeof x === 'number') {
  5. return Number(x.toString().split('').reverse().join(''));
  6. } else if (typeof x === 'string') {
  7. return x.split('').reverse().join('');
  8. }
  9. }

上例中,我们重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。

注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

类型谓词

谓词为 parameterName is Type 这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

  1. interface Bird {
  2. fly();
  3. layEggs();
  4. }
  5. interface Fish {
  6. swim();
  7. layEggs();
  8. }
  9. function isFish(animal: Fish | Bird): animal is Fish {
  10. return (<Fish>animal).swim !== undefined;
  11. }
  12. // 'swim' 和 'fly' 调用都没有问题
  13. let pet = {
  14. fly() {
  15. console.log('pet fly')
  16. },
  17. layEggs() {
  18. console.log('pet layEggs')
  19. }
  20. }
  21. if (isFish(pet)) {
  22. pet.swim();
  23. } else {
  24. pet.fly();
  25. }

接口

在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

接口一般首字母大写。建议接口的名称加上 I 前缀。

  1. interface IPerson {
  2. name: string;
  3. age: number;
  4. }
  5. let tom: IPerson = {
  6. name: 'Quanzaiyu',
  7. age: 24
  8. };

实现接口后,不允许往对象中添加未定义的属性,也不允许少一些属性

可选值

通过 ? 设置接口的属性是可选的

  1. interface SquareConfig {
  2. color?: string;
  3. width?: number;
  4. }
  5. function createSquare(config: SquareConfig): { color: string = "white"; area: number = 100 } {
  6. let newSquare = {color: "white", area: 100};
  7. if (config.color) {
  8. newSquare.color = config.color;
  9. }
  10. if (config.width) {
  11. newSquare.area = config.width * config.width;
  12. }
  13. return newSquare;
  14. }
  15. let mySquare = createSquare({color: "black"});

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。可以在属性名前用 readonly来指定只读属性:

  1. interface Point {
  2. readonly x: number;
  3. readonly y: number;
  4. }
  5. let p1: Point = { x: 10, y: 20 };
  6. p1.x = 5; // error!

函数类型

接口能够描述JavaScript中对象拥有的各种各样的外形。除了描述带有属性的普通对象外,接口也可以描述函数类型。

  1. interface SearchFunc {
  2. (source: string, subString: string): boolean;
  3. }
  4. let mySearch: SearchFunc = function(source: string, subString: string) {
  5. let result = source.search(subString);
  6. return result > -1;
  7. }

以上定义了一个名为 SearchFunc 的接口, 可传入两个参数都为 string 类型, 返回值为 boolean 类型

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:

  1. let mySearch: SearchFunc;
  2. mySearch = function(src: string, sub: string): boolean {
  3. let result = src.search(sub);
  4. return result > -1;
  5. }

接口继承

通过 extends 关键字进行接口继承

  1. interface Shape {
  2. color: string;
  3. }
  4. interface Square extends Shape {
  5. sideLength: number;
  6. }
  7. let square = <Square>{};
  8. square.color = "blue";
  9. square.sideLength = 10;

这句 let square = <Square>{}, 将一个对象强制使用Square进行约束, 同 let square: Square

多继承:

  1. interface Shape {
  2. color: string;
  3. }
  4. interface PenStroke {
  5. penWidth: number;
  6. }
  7. interface Square extends Shape, PenStroke {
  8. sideLength: number;
  9. }
  10. let square = <Square>{};
  11. square.color = "blue";
  12. square.sideLength = 10;
  13. square.penWidth = 5.0;

接口实现

接口描述了类的公共部分,而不是公共和私有两部分。

当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,不在检查的范围内。

  1. interface ClockInterface {
  2. currentTime: Date;
  3. setTime(d: Date);
  4. getTime();
  5. }
  6. class Clock implements ClockInterface {
  7. currentTime: Date;
  8. setTime(d: Date) {
  9. this.currentTime = d;
  10. }
  11. getTime() {
  12. return this.currentTime.toLocaleString()
  13. }
  14. constructor(y: number, M: number, d: number, h?: number, m?: number, s?: number) {
  15. this.currentTime = new Date(y, M - 1, d, h, m, s)
  16. }
  17. }
  18. let clock : Clock = new Clock(2019, 6, 8, 8, 30, 20)
  19. console.log(clock.getTime()) // 2019/6/8 上午8:30:20
  20. clock.setTime(new Date())
  21. console.log(clock.getTime()) // 2019/5/16 下午10:02:20

接口合并

接口中的属性在合并时会简单的合并到一个接口中:

  1. interface Alarm {
  2. price: number;
  3. }
  4. interface Alarm {
  5. weight: number;
  6. }

相当于:

  1. interface Alarm {
  2. price: number;
  3. weight: number;
  4. }

注意,合并的属性的类型必须是唯一的

  1. interface Alarm {
  2. price: number;
  3. }
  4. interface Alarm {
  5. price: number; // 虽然重复了,但是类型都是 `number`,所以不会报错
  6. weight: number;
  7. }
  1. interface Alarm {
  2. price: number;
  3. }
  4. interface Alarm {
  5. price: string; // 类型不一致,会报错
  6. weight: number;
  7. }
  8. // index.ts(5,3): error TS2403: Subsequent variable declarations must have the same type. Variable 'price' must be of type 'number', but here has type 'string'.

接口中方法的合并,与函数的重载一样:

  1. interface Alarm {
  2. price: number;
  3. alert(s: string): string;
  4. }
  5. interface Alarm {
  6. weight: number;
  7. alert(s: string, n: number): string;
  8. }

相当于:

  1. interface Alarm {
  2. price: number;
  3. weight: number;
  4. alert(s: string): string;
  5. alert(s: string, n: number): string;
  6. }

索引签名

TypeScript支持两种索引签名:字符串和数字。

普通对象实现的签名

要创建一个JS的普通对象,可以使用以下形式的签名接口:

  1. interface NormalObj {
  2. [propName: string]: any;
  3. }
  4. let obj: NormalObj = {
  5. a: 'red',
  6. b: 10,
  7. c: {
  8. a: 1
  9. }
  10. }
  11. console.log(obj.c.a) // 1

数组实现的签名

要创建一个数组,可以不使用泛型,而是通过索引签名接口的方式创建:

  1. // 类数组对象或数组签名
  2. interface IndexedObj {
  3. [index: number]: any;
  4. }
  5. let array: IndexedObj = [1, 'Bob']
  6. console.log(array[0]) // 1
  7. console.log(array[1]) // 'Bob'

由于数组的每一项都拥有下标,因此可以直接以数组实现此接口,当然也可以以类数组对象实现此接口:

  1. let objArray: IndexedObj = {
  2. 1: 'Bob',
  3. 5: 1
  4. }
  5. console.log(array[1]) // 'Bob'
  6. console.log(array[5]) // 1

也可指定值的类型:

  1. interface StringArray {
  2. [index: number]: string;
  3. }
  4. let myArray: StringArray = ['Bob', 'Fred']
  5. let objArray: StringArray = {
  6. 1: 'Bob',
  7. 5: 'Fred'
  8. };

带其他字段的索引签名

字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。因为字符串索引声明了 obj.propertyobj["property"] 两种形式都可以。

如果指定的属性与类型索引不一致则会报错:

  1. interface Person {
  2. name: string;
  3. age: number; // 错误,`age`的类型与索引类型返回值的类型不匹配
  4. [propName: string]: string;
  5. }

只读索引

可以将索引签名设置为只读,这样就防止了给索引赋值:

  1. interface ReadonlyStringArray {
  2. readonly [index: number]: string;
  3. }
  4. let myArray: ReadonlyStringArray = ["Alice", "Bob"];
  5. myArray[2] = "Mallory"; // error!
  6. myArray = ["Alice", "Bob", 'July'] // okay

不能设置 myArray[2],因为索引签名是只读的,而整体重新赋值是可以的。

继承与重写

子类通过 extends 继承父类,如果要使用父类的构造方法则可以使用 super()

  1. class Animal {
  2. private name: string
  3. constructor(name: string) {
  4. this.name = name
  5. }
  6. move() {
  7. console.log(`${this.name} moved`)
  8. }
  9. bark() {
  10. console.log('Animal bark')
  11. }
  12. }
  13. class Dog extends Animal {
  14. constructor(name: string) {
  15. console.log('=== created a dog ===')
  16. super(name)
  17. }
  18. bark() {
  19. console.log('Dog bark')
  20. }
  21. }
  22. let dog = new Dog('Luck') // === created a dog ===
  23. dog.bark() // Dog bark
  24. dog.move() // Luck moved

编译结果(ES5):

  1. "use strict";
  2. var __extends = (this && this.__extends) || (function () {
  3. var extendStatics = function (d, b) {
  4. extendStatics = Object.setPrototypeOf ||
  5. ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
  6. function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
  7. return extendStatics(d, b);
  8. };
  9. return function (d, b) {
  10. extendStatics(d, b);
  11. function __() { this.constructor = d; }
  12. d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
  13. };
  14. })();
  15. var Animal = /** @class */ (function () {
  16. function Animal(name) {
  17. this.name = name;
  18. }
  19. Animal.prototype.move = function () {
  20. console.log(this.name + " moved");
  21. };
  22. Animal.prototype.bark = function () {
  23. console.log('Animal bark');
  24. };
  25. return Animal;
  26. }());
  27. var Dog = /** @class */ (function (_super) {
  28. __extends(Dog, _super);
  29. function Dog(name) {
  30. var _this = this;
  31. console.log('=== created a dog ===');
  32. _this = _super.call(this, name) || this;
  33. return _this;
  34. }
  35. Dog.prototype.bark = function () {
  36. console.log('Dog bark');
  37. };
  38. return Dog;
  39. }(Animal));
  40. var dog = new Dog('Luck'); // === created a dog ===
  41. dog.bark(); // Dog bark
  42. dog.move(); // Luck moved

权限修饰符

  • **public** 默认,公有的,外部可访问
  • **private** 私有的,外部不可访问
  • protected 受保护的,子类中可使用

readonly

只读的,只读属性必须在声明时或构造函数里被初始化,比如:

  1. class Octopus {
  2. readonly name: string;
  3. constructor (theName: string) {
  4. this.name = theName;
  5. }
  6. }
  7. let dad = new Octopus("Man with the 8 strong legs");
  8. dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

getter & setter

通过 getter 和 setter 可以很方便对属性进行操作,同时可以添加一些附加操作

  1. class Employee {
  2. private _fullName: string;
  3. private _firstName: string;
  4. private _lastName: string;
  5. constructor(firstName: string = '', lastName: string = '') {
  6. this._firstName = firstName
  7. this._lastName = lastName
  8. this._fullName = this._firstName + ' ' + this._lastName
  9. }
  10. get firstName(): string {
  11. return this._firstName;
  12. }
  13. set firstName(newName: string) {
  14. this._firstName = newName;
  15. this._fullName = this._lastName ? this._firstName + ' ' + this._lastName : this._firstName
  16. }
  17. get lastName(): string {
  18. return this._lastName;
  19. }
  20. set lastName(newName: string) {
  21. this._lastName = newName;
  22. this._fullName = this._firstName ? this._firstName + ' ' + this._lastName : this._firstName
  23. }
  24. get fullName(): string {
  25. return this._fullName;
  26. }
  27. }
  28. let employee = new Employee();
  29. employee.firstName = "Bob";
  30. employee.lastName = "Smith";
  31. console.log(employee.fullName) // Bob Smith

编译结果(ES5):

  1. "use strict";
  2. var Employee = /** @class */ (function () {
  3. function Employee(firstName, lastName) {
  4. if (firstName === void 0) { firstName = ''; }
  5. if (lastName === void 0) { lastName = ''; }
  6. this._firstName = firstName;
  7. this._lastName = lastName;
  8. this._fullName = this._firstName + ' ' + this._lastName;
  9. }
  10. Object.defineProperty(Employee.prototype, "firstName", {
  11. get: function () {
  12. return this._firstName;
  13. },
  14. set: function (newName) {
  15. this._firstName = newName;
  16. this._fullName = this._lastName ? this._firstName + ' ' + this._lastName : this._firstName;
  17. },
  18. enumerable: false,
  19. configurable: true
  20. });
  21. Object.defineProperty(Employee.prototype, "lastName", {
  22. get: function () {
  23. return this._lastName;
  24. },
  25. set: function (newName) {
  26. this._lastName = newName;
  27. this._fullName = this._firstName ? this._firstName + ' ' + this._lastName : this._firstName;
  28. },
  29. enumerable: false,
  30. configurable: true
  31. });
  32. Object.defineProperty(Employee.prototype, "fullName", {
  33. get: function () {
  34. return this._fullName;
  35. },
  36. enumerable: false,
  37. configurable: true
  38. });
  39. return Employee;
  40. }());
  41. var employee = new Employee();
  42. employee.firstName = "Bob";
  43. employee.lastName = "Smith";
  44. console.log(employee.fullName); // Bob Smith

编译结果(ES6):

  1. "use strict";
  2. class Employee {
  3. constructor(firstName = '', lastName = '') {
  4. this._firstName = firstName;
  5. this._lastName = lastName;
  6. this._fullName = this._firstName + ' ' + this._lastName;
  7. }
  8. get firstName() {
  9. return this._firstName;
  10. }
  11. set firstName(newName) {
  12. this._firstName = newName;
  13. this._fullName = this._lastName ? this._firstName + ' ' + this._lastName : this._firstName;
  14. }
  15. get lastName() {
  16. return this._lastName;
  17. }
  18. set lastName(newName) {
  19. this._lastName = newName;
  20. this._fullName = this._firstName ? this._firstName + ' ' + this._lastName : this._firstName;
  21. }
  22. get fullName() {
  23. return this._fullName;
  24. }
  25. }
  26. let employee = new Employee();
  27. employee.firstName = "Bob";
  28. employee.lastName = "Smith";
  29. console.log(employee.fullName); // Bob Smith

这也是Vue中计算属性的实现方式。

静态属性

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。比较两个类类型的对象时,只有实例的成员会被比较。静态成员和构造函数不在比较的范围内。

通过 static 声明的属性为静态属性,通过类名调用

  1. class Grid {
  2. static origin = {x: 0, y: 0};
  3. }
  4. console.log(Grid.origin)

编译结果(ES5):

  1. "use strict";
  2. var Grid = /** @class */ (function () {
  3. function Grid() {
  4. }
  5. Grid.origin = { x: 0, y: 0 };
  6. return Grid;
  7. }());
  8. console.log(Grid.origin);

编译结果(ES6):

  1. "use strict";
  2. class Grid {
  3. }
  4. Grid.origin = { x: 0, y: 0 };
  5. console.log(Grid.origin);

抽象类

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。

  1. abstract class Animal {
  2. abstract makeSound(): void;
  3. move(): void {
  4. console.log('move...');
  5. }
  6. }
  7. class Dog extends Animal {
  8. // 必须实现抽象方法
  9. makeSound() {
  10. console.log('Dog bark');
  11. }
  12. }
  13. let dog = new Dog() // okay
  14. let animal = new Animal() // Error 不能直接实例化抽象类

类实现接口

实现(implements)是面向对象中的一个重要概念。一般来讲,一个类只能继承自另一个类,有时候不同类之间可以有一些共有的特性,这时候就可以把特性提取成接口(interfaces),用 implements 关键字来实现。这个特性大大提高了面向对象的灵活性。

举例来说,门是一个类,防盗门是门的子类。如果防盗门有一个报警器的功能,我们可以简单的给防盗门添加一个报警方法。这时候如果有另一个类,车,也有报警器的功能,就可以考虑把报警器提取出来,作为一个接口,防盗门和车都去实现它:

  1. interface Alarm {
  2. alert(): void;
  3. }
  4. class Door {}
  5. class SecurityDoor extends Door implements Alarm {
  6. alert() {
  7. console.log('SecurityDoor alert');
  8. }
  9. }
  10. class Car implements Alarm {
  11. alert() {
  12. console.log('Car alert');
  13. }
  14. }

一个类可以实现多个接口:

  1. interface Alarm {
  2. alert();
  3. }
  4. interface Light {
  5. lightOn();
  6. lightOff();
  7. }
  8. class Car implements Alarm, Light {
  9. alert() {
  10. console.log('Car alert');
  11. }
  12. lightOn() {
  13. console.log('Car light on');
  14. }
  15. lightOff() {
  16. console.log('Car light off');
  17. }
  18. }

上例中,Car 实现了 AlarmLight 接口,既能报警,也能开关车灯。

把类当做接口使用

接口可以直接继承类:

  1. class Point {
  2. x: number;
  3. y: number;
  4. }
  5. interface Point3d extends Point {
  6. z: number;
  7. }
  8. let point3d: Point3d = {x: 1, y: 2, z: 3};