变量
联合类型声明
为变量指定多个可能的类型:
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() // okay
func('Hello') // okay
func('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:20
clock.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]) // 1
console.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: string
constructor(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 bark
dog.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 bark
dog.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 = firstName
this._lastName = lastName
this._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() // okay
let 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};