变量
联合类型声明
为变量指定多个可能的类型:
let a : number | string
在解构中的应用
let o = { a: 'Hello', b: 2019 }let { a: c, b: d }: { a: string, b: number } = o;
编译成JS:
var o = { a: 'Hello', b: 2019}var c = o.a, d = o.b;
同以下JS语法:
let { a: c, b: d } = o;
其实也就是将 o 解构的同时, 重新将 a 和 b 命名为 c 和 d 。
函数
在 JavaScript 中,有两种常见的定义函数的方式——函数声明(Function Declaration)和函数表达式(Function Expression):
// 函数声明(Function Declaration)function sum(x, y) {return x + y;}// 函数表达式(Function Expression)let mySum = function (x, y) {return x + y;};
下面说说在TS中如何为函数参数和返回值添加类型约束。
定义函数的几种方式
一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义较简单:
// 命名函数function myAdd(x: number, y: number): number {return x + y;}// 匿名函数let myAdd = function(x: number, y: number): number { return x + y; };// 箭头函数let myAdd = (x: number, y: number): number => x + y// 完整的声明let myAdd: (x: number, y: number) => number =function(x: number, y: number): number { return x + y; };
用接口定义函数的形状
我们也可以使用接口的方式来定义一个函数需要符合的形状:
interface SearchFunc {(source: string, subString: string): boolean;}let mySearch: SearchFunc = function(source: string, subString: string) {return source.search(subString) !== -1;}
可选参数
function func(a: string, b?: string) {return a + b}func('Hello')func('Hello', 'world')func() // Error: Expected 1-2 arguments, but got 0.
需要注意的是,可选参数必须接在必需参数后面。换句话说,可选参数后面不允许再出现必须参数了。
默认参数
function func(a: string = 'Hi', b?: string) {return a + b}func() // okayfunc('Hello') // okayfunc('Hello', 'world') // okay
如果给一个参数设置了默认值,则也不需要显性传递参数。
但是如果第二个参数不是可选的, 则仍然报错:
function func(a: string = 'Hi', b: string) {return a + b}console.log(func('Hello', 'world'))console.log(func()) // Error: Expected 2 arguments, but got 0.console.log(func('Hello')) // Error: Expected 2 arguments, but got 1.
剩余参数
可以使用 ...rest 的方式获取函数中的剩余参数(rest 参数)
function buildName(firstName: string, ...restOfName: string[]) {return firstName + " " + restOfName.join(" ");}let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
函数重载
重载允许一个函数接受不同数量或类型的参数时,作出不同的处理。
比如,我们需要实现一个函数 reverse,输入数字 123 的时候,输出反转的数字 321,输入字符串 'hello' 的时候,输出反转的字符串 'olleh'。
利用联合类型,我们可以这么实现:
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('');}}
然而这样有一个缺点,就是不能够精确的表达,输入为数字的时候,输出也应该为数字,输入为字符串的时候,输出也应该为字符串。
这时,我们可以使用重载定义多个 reverse 的函数类型:
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('');}}
上例中,我们重复定义了多次函数 reverse,前几次都是函数定义,最后一次是函数实现。在编辑器的代码提示中,可以正确的看到前两个提示。
注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。
类型谓词
谓词为 parameterName is Type 这种形式, parameterName必须是来自于当前函数签名里的一个参数名。
interface Bird {fly();layEggs();}interface Fish {swim();layEggs();}function isFish(animal: Fish | Bird): animal is Fish {return (<Fish>animal).swim !== undefined;}// 'swim' 和 'fly' 调用都没有问题let pet = {fly() {console.log('pet fly')},layEggs() {console.log('pet layEggs')}}if (isFish(pet)) {pet.swim();} else {pet.fly();}
接口
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。
在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。
接口一般首字母大写。建议接口的名称加上 I 前缀。
interface IPerson {name: string;age: number;}let tom: IPerson = {name: 'Quanzaiyu',age: 24};
实现接口后,不允许往对象中添加未定义的属性,也不允许少一些属性
可选值
通过 ? 设置接口的属性是可选的
interface SquareConfig {color?: string;width?: number;}function createSquare(config: SquareConfig): { color: string = "white"; area: number = 100 } {let newSquare = {color: "white", area: 100};if (config.color) {newSquare.color = config.color;}if (config.width) {newSquare.area = config.width * config.width;}return newSquare;}let mySquare = createSquare({color: "black"});
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。可以在属性名前用 readonly来指定只读属性:
interface Point {readonly x: number;readonly y: number;}let p1: Point = { x: 10, y: 20 };p1.x = 5; // error!
函数类型
接口能够描述JavaScript中对象拥有的各种各样的外形。除了描述带有属性的普通对象外,接口也可以描述函数类型。
interface SearchFunc {(source: string, subString: string): boolean;}let mySearch: SearchFunc = function(source: string, subString: string) {let result = source.search(subString);return result > -1;}
以上定义了一个名为 SearchFunc 的接口, 可传入两个参数都为 string 类型, 返回值为 boolean 类型
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:
let mySearch: SearchFunc;mySearch = function(src: string, sub: string): boolean {let result = src.search(sub);return result > -1;}
接口继承
通过 extends 关键字进行接口继承
interface Shape {color: string;}interface Square extends Shape {sideLength: number;}let square = <Square>{};square.color = "blue";square.sideLength = 10;
这句 let square = <Square>{}, 将一个对象强制使用Square进行约束, 同 let square: Square
多继承:
interface Shape {color: string;}interface PenStroke {penWidth: number;}interface Square extends Shape, PenStroke {sideLength: number;}let square = <Square>{};square.color = "blue";square.sideLength = 10;square.penWidth = 5.0;
接口实现
接口描述了类的公共部分,而不是公共和私有两部分。
当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,不在检查的范围内。
interface ClockInterface {currentTime: Date;setTime(d: Date);getTime();}class Clock implements ClockInterface {currentTime: Date;setTime(d: Date) {this.currentTime = d;}getTime() {return this.currentTime.toLocaleString()}constructor(y: number, M: number, d: number, h?: number, m?: number, s?: number) {this.currentTime = new Date(y, M - 1, d, h, m, s)}}let clock : Clock = new Clock(2019, 6, 8, 8, 30, 20)console.log(clock.getTime()) // 2019/6/8 上午8:30:20clock.setTime(new Date())console.log(clock.getTime()) // 2019/5/16 下午10:02:20
接口合并
接口中的属性在合并时会简单的合并到一个接口中:
interface Alarm {price: number;}interface Alarm {weight: number;}
相当于:
interface Alarm {price: number;weight: number;}
注意,合并的属性的类型必须是唯一的:
interface Alarm {price: number;}interface Alarm {price: number; // 虽然重复了,但是类型都是 `number`,所以不会报错weight: number;}
interface Alarm {price: number;}interface Alarm {price: string; // 类型不一致,会报错weight: number;}// 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'.
接口中方法的合并,与函数的重载一样:
interface Alarm {price: number;alert(s: string): string;}interface Alarm {weight: number;alert(s: string, n: number): string;}
相当于:
interface Alarm {price: number;weight: number;alert(s: string): string;alert(s: string, n: number): string;}
索引签名
TypeScript支持两种索引签名:字符串和数字。
普通对象实现的签名
要创建一个JS的普通对象,可以使用以下形式的签名接口:
interface NormalObj {[propName: string]: any;}let obj: NormalObj = {a: 'red',b: 10,c: {a: 1}}console.log(obj.c.a) // 1
数组实现的签名
要创建一个数组,可以不使用泛型,而是通过索引签名接口的方式创建:
// 类数组对象或数组签名interface IndexedObj {[index: number]: any;}let array: IndexedObj = [1, 'Bob']console.log(array[0]) // 1console.log(array[1]) // 'Bob'
由于数组的每一项都拥有下标,因此可以直接以数组实现此接口,当然也可以以类数组对象实现此接口:
let objArray: IndexedObj = {1: 'Bob',5: 1}console.log(array[1]) // 'Bob'console.log(array[5]) // 1
也可指定值的类型:
interface StringArray {[index: number]: string;}let myArray: StringArray = ['Bob', 'Fred']let objArray: StringArray = {1: 'Bob',5: 'Fred'};
带其他字段的索引签名
字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。因为字符串索引声明了 obj.property 和 obj["property"] 两种形式都可以。
如果指定的属性与类型索引不一致则会报错:
interface Person {name: string;age: number; // 错误,`age`的类型与索引类型返回值的类型不匹配[propName: string]: string;}
只读索引
可以将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray {readonly [index: number]: string;}let myArray: ReadonlyStringArray = ["Alice", "Bob"];myArray[2] = "Mallory"; // error!myArray = ["Alice", "Bob", 'July'] // okay
不能设置 myArray[2],因为索引签名是只读的,而整体重新赋值是可以的。
类
继承与重写
子类通过 extends 继承父类,如果要使用父类的构造方法则可以使用 super()
class Animal {private name: stringconstructor(name: string) {this.name = name}move() {console.log(`${this.name} moved`)}bark() {console.log('Animal bark')}}class Dog extends Animal {constructor(name: string) {console.log('=== created a dog ===')super(name)}bark() {console.log('Dog bark')}}let dog = new Dog('Luck') // === created a dog ===dog.bark() // Dog barkdog.move() // Luck moved
编译结果(ES5):
"use strict";var __extends = (this && this.__extends) || (function () {var extendStatics = function (d, b) {extendStatics = Object.setPrototypeOf ||({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };return extendStatics(d, b);};return function (d, b) {extendStatics(d, b);function __() { this.constructor = d; }d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());};})();var Animal = /** @class */ (function () {function Animal(name) {this.name = name;}Animal.prototype.move = function () {console.log(this.name + " moved");};Animal.prototype.bark = function () {console.log('Animal bark');};return Animal;}());var Dog = /** @class */ (function (_super) {__extends(Dog, _super);function Dog(name) {var _this = this;console.log('=== created a dog ===');_this = _super.call(this, name) || this;return _this;}Dog.prototype.bark = function () {console.log('Dog bark');};return Dog;}(Animal));var dog = new Dog('Luck'); // === created a dog ===dog.bark(); // Dog barkdog.move(); // Luck moved
权限修饰符
**public**默认,公有的,外部可访问**private**私有的,外部不可访问protected受保护的,子类中可使用
readonly
只读的,只读属性必须在声明时或构造函数里被初始化,比如:
class Octopus {readonly name: string;constructor (theName: string) {this.name = theName;}}let dad = new Octopus("Man with the 8 strong legs");dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.
getter & setter
通过 getter 和 setter 可以很方便对属性进行操作,同时可以添加一些附加操作
class Employee {private _fullName: string;private _firstName: string;private _lastName: string;constructor(firstName: string = '', lastName: string = '') {this._firstName = firstNamethis._lastName = lastNamethis._fullName = this._firstName + ' ' + this._lastName}get firstName(): string {return this._firstName;}set firstName(newName: string) {this._firstName = newName;this._fullName = this._lastName ? this._firstName + ' ' + this._lastName : this._firstName}get lastName(): string {return this._lastName;}set lastName(newName: string) {this._lastName = newName;this._fullName = this._firstName ? this._firstName + ' ' + this._lastName : this._firstName}get fullName(): string {return this._fullName;}}let employee = new Employee();employee.firstName = "Bob";employee.lastName = "Smith";console.log(employee.fullName) // Bob Smith
编译结果(ES5):
"use strict";var Employee = /** @class */ (function () {function Employee(firstName, lastName) {if (firstName === void 0) { firstName = ''; }if (lastName === void 0) { lastName = ''; }this._firstName = firstName;this._lastName = lastName;this._fullName = this._firstName + ' ' + this._lastName;}Object.defineProperty(Employee.prototype, "firstName", {get: function () {return this._firstName;},set: function (newName) {this._firstName = newName;this._fullName = this._lastName ? this._firstName + ' ' + this._lastName : this._firstName;},enumerable: false,configurable: true});Object.defineProperty(Employee.prototype, "lastName", {get: function () {return this._lastName;},set: function (newName) {this._lastName = newName;this._fullName = this._firstName ? this._firstName + ' ' + this._lastName : this._firstName;},enumerable: false,configurable: true});Object.defineProperty(Employee.prototype, "fullName", {get: function () {return this._fullName;},enumerable: false,configurable: true});return Employee;}());var employee = new Employee();employee.firstName = "Bob";employee.lastName = "Smith";console.log(employee.fullName); // Bob Smith
编译结果(ES6):
"use strict";class Employee {constructor(firstName = '', lastName = '') {this._firstName = firstName;this._lastName = lastName;this._fullName = this._firstName + ' ' + this._lastName;}get firstName() {return this._firstName;}set firstName(newName) {this._firstName = newName;this._fullName = this._lastName ? this._firstName + ' ' + this._lastName : this._firstName;}get lastName() {return this._lastName;}set lastName(newName) {this._lastName = newName;this._fullName = this._firstName ? this._firstName + ' ' + this._lastName : this._firstName;}get fullName() {return this._fullName;}}let employee = new Employee();employee.firstName = "Bob";employee.lastName = "Smith";console.log(employee.fullName); // Bob Smith
这也是Vue中计算属性的实现方式。
静态属性
类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。比较两个类类型的对象时,只有实例的成员会被比较。静态成员和构造函数不在比较的范围内。
通过 static 声明的属性为静态属性,通过类名调用
class Grid {static origin = {x: 0, y: 0};}console.log(Grid.origin)
编译结果(ES5):
"use strict";var Grid = /** @class */ (function () {function Grid() {}Grid.origin = { x: 0, y: 0 };return Grid;}());console.log(Grid.origin);
编译结果(ES6):
"use strict";class Grid {}Grid.origin = { x: 0, y: 0 };console.log(Grid.origin);
抽象类
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。
abstract class Animal {abstract makeSound(): void;move(): void {console.log('move...');}}class Dog extends Animal {// 必须实现抽象方法makeSound() {console.log('Dog bark');}}let dog = new Dog() // okaylet animal = new Animal() // Error 不能直接实例化抽象类
类实现接口
实现(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();}interface Light {lightOn();lightOff();}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 接口,既能报警,也能开关车灯。
把类当做接口使用
接口可以直接继承类:
class Point {x: number;y: number;}interface Point3d extends Point {z: number;}let point3d: Point3d = {x: 1, y: 2, z: 3};
