跳到主要内容

TS 接口、类、函数

示例代码:typescript-demos

接口

在 TS 中,接口的作用就是为类型命名和为你的代码或第三方代码定义契约。1

使用示例

interface User {    // 定义user接口类型
name: string,
age: number,
}
// const getUserName = (user) => user.name; // Error: 参数“user”隐式具有“any”类型
const getUserName = (user: User) => user.name; // OK
console.log(getUserName({ name: 'Tony', age: 18 })); // OK

可选属性

? 定义可选属性

interface User {
name: string,
age?: number, // here
}
const getUserName = (user: User) => user.name; // OK
console.log(getUserName({ name: 'Tony' })); // OK

只读属性

readonly 定义只读属性

interface User {
name: string,
readonly age: number, // here
}
const getUserName = (user: User) => user.age++; // Error: 无法分配到 "age" ,因为它是只读属性。

函数类型

方式 1:直接在内部描述函数

interface User {
name: string,
greet: (username: string) => string
}
const user: User = {
name: 'Tony',
greet: (username: string) => `Hi~${username}!`
}
console.log(user.greet('Jack')); // OK

方式 2:先用接口描述函数类型,再在内部使用该函数类型接口

interface Greet {   // here
(username: string) : string
}

interface User {
name: string,
greet: Greet // here
}
const user: User = {
name: 'Tony',
greet: (username: string) => `Hi~${username}!`
}
console.log(user.greet('Jack')); // OK

可索引类型

可索引类型具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。

interface Email {
[name: string]: string // here
}

interface User {
name: string,
age?: number,
readonly isMale: boolean,
say: () => string,
email: Email // here
}

const user: User = {
name: 'Tony',
age: 18,
isMale: true,
say: Function,
email: {
qq: 'tony@qq.com',
foxmail: 'tony@foxmail.com',
gmail: 'tony@gmail.com',
}
}

属性检查

一个示例:

interface SquareConfig {
color?: string;
width?: number;
}

function createSquare(config: SquareConfig) : { color: string; area : number } {
return {
color: config.color ? config.color : 'white',
area: config.width ? config.width * config.width : 0,
}
}

let square = createSquare({ color: 'green', width: 50 }); // OK

但是如果传参对象出现一个非目标类型属性名 colour

let square = createSquare({ colour: 'red', width: 100 });    // Error: “colour”中不存在类型“SquareConfig”

这时有 3 种处理方式:

  1. 使用类型断言
  2. 使用字符串索引签名
  3. 将字面量赋值给另外一个变量

方法 1:使用类型断言

let square = createSquare({ colour: 'red', width: 100 } as SquareConfig);   // OK

方法 2:使用字符串索引签名(只要新属性不叫color、width, 什么类型也无所谓)

interface SquareConfig {    // 修改接口
color?: string;
width?: number;
[propName: string]: any; // here
}
let square = createSquare({ colour: 'red', width: 100 }); // OK

方法 3:将字面量赋值给另外一个变量(本质上转化为any类型,不推荐)

let options: any = { colour: 'red', width: 100 };
let square = createSquare(options); // OK

继承接口

和类一样,接口也可以继承(从一个接口里复制成员到另一个接口里)。

interface User {    // 定义user接口类型
name: string,
age: number,
} // name, age

interface VipUser extends User { // 继承单个接口
color: number
} // name, age, color

interface SuperVipUser extends User, VipUser { // 继承多个接口
assistant: string
} // name, age, color, assistant

  1. 传统的面向对象语言一般都是基于类,但 JS 是基于原型
  2. ES6 之后,JS 可以用 class 了
  3. TS 可以弥补 JS Class 还没有的一些特性,如修饰符、抽象类等

示例

class Greeter {
greeting: string; // 属性
constructor(message: string) { // 构造函数
this.greeting = message;
}
greet() { // 方法
return "Hello, " + this.greeting;
}
}

let greeter = new Greeter("world");
greeter.greet(); // OK

继承

子类通过 extends 关键字继承父类。

class Animal {  // 基类(父类)
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}

class Dog extends Animal { // 派生类(子类)
bark() {
console.log('Woof! Woof!');
}
}

const dog = new Dog();
dog.bark(); // OK
dog.move(10); // OK
dog.bark(); // OK

注意:

  1. 当派生类包含了一个构造函数,它必须调用 super()(它会执行基类的构造函数)
  2. 在构造函数里访问 this 的属性之前,一定要调用 super
// 基类:Animal
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}

// 派生类:Snake
class Snake extends Animal {
constructor(name: string) {
super(name); // 调用基类构造方法
}
move(distanceInMeters = 5) { // 重写父类方法
console.log('Slithering...');
super.move(distanceInMeters);
}
}

// 派生类:Horse
class Horse extends Animal {
constructor(name: string) {
super(name); // 调用基类构造方法
}
move(distanceInMeters = 45) { // 重写父类方法
console.log('Galloping...');
super.move(distanceInMeters);
}
}

let sam: Horse = new Snake('Sammy the Python'); // 即使声明为`Horse`类型,还是会调用`Snake`里重写方法
let tom: Animal = new Horse('Tommy the Palomino'); // 即使声明为`Animal`类型,还是会调用`Horse`里重写方法

sam.move(); // OK
tom.move(34); // OK

访问限定符

访问限定符类内部子类类外部备注
publicOKOKOK默认
protectedOKOK--
privateOK---

public

class Car {
public run() {
console.log('启动...');
}
}

const car = new Car();

car.run(); // OK, 类外部使用

protected

class Car {
protected run() {
console.log('启动...')
}
}

class GTR extends Car {
init() {
this.run();
}
}

const car = new Car();
const gtr = new GTR();

car.run(); // Error, 类外部不能使用
gtr.init(); // OK,子类可用
gtr.run(); // Error,该方法只能在Car类内部或其子类访问

private

class Car {
private run() {
console.log('启动...');
}
}

const car = new Car();

car.run(); // Error, 私有属性只能Car类内部使用

存取器

不使用存取器示例:

class Employee {
fullName!: string;
}

let employee = new Employee();
employee.fullName = "Bob Smith"; // 可以随意更改(方便但会有麻烦)
if (employee.fullName) {
console.log(employee.fullName);
}

使用存取器示例示例:

let passcode!: string;
class Employee {
private _fullName!: string; // `!`赋值断言(见下)

get fullName(): string {
return this._fullName;
}
set fullName(value: string) {
if (passcode && passcode === 'secret') { // 密码不匹配,则不允许修改
this._fullName = value;
} else {
console.log('Error, Unauthorized update of employee!');
}
}
}

const setName = (name: string) => {
let employee = new Employee();
employee.fullName = name;
if (employee.fullName) {
console.log(employee.fullName);
}
}

passcode = 'secret';
setName('Tony'); // OK

passcode = 'hahaha';
setName('Marry'); // Error, Unauthorized update of employee!

注 1:只带有 get 不带有 set 的存取器自动被推断为 readonly

注 2:VSCode 支持快捷生成存取器:

getter-and-setter

注 3:赋值断言2

TypeScript 2.7 引入了一个新的控制严格性的标记 --strictPropertyInitialization,使用这个标记会确保类的每个实例属性都会在构造函数里或使用属性初始化器赋值:

// 报错
baz: boolean; // Error! Property 'baz' has no initializer and is not assigned directly in the constructor.

// 处理方式 1
baz: boolean | undefined;

// 处理方式 2(显式赋值断言)
baz!: boolean;

静态属性

说明

  1. ES6 规定,Class 内部只有静态方法,没有静态属性,但有一个提案 3 提供了类的静态属性
  2. TS 实现了类的静态属性

静态和实例

  1. 类中的成员分为:静态成员实例成员
  2. 静态成员包含了:静态属性静态方法
  3. 实例成员包含了:实例属性实例方法

区别

  1. 静态成员 前面需要添加修饰符 static
  2. 静态成员 使用 类名 来调用,实例成员使用 this 来调用
  3. 静态成员 不会被实例继承,只能通过类来调用

实例成员示例

// 创建 Foo
class Foo {
str = 'Hello'; // 实例属性
constructor() {
console.log(this.str);
}
classMethod() { // 实例方法
console.log('method');
}
}

// 创建 Foo 实例:foo
// 1. 由于会调用Foo构造函数,所以会输出Hello
const foo = new Foo(); // => Hello

// 创建 Bar 并继承自 Foo
class Bar extends Foo {
constructor() {
super();
}
}

// 创建 Bar 实例
const bar = new Bar(); // => Hello

// 访问基类实例成员
console.log(bar.str); // => Hello
bar.classMethod(); // => method

静态成员示例


class Foo {
static str = 'Hello';
constructor() {
// console.log(this.str); // Error: 属性“str”在类型“Foo”上不存在。你的意思是改为访问静态成员“Foo.str”吗?
console.log(Foo.str); // 用类名访问
}
static classMethod() {
console.log('Method');
}
}

const foo = new Foo(); // => Hello

class Bar extends Foo {
static barStr = 'Hello Bar';
constructor() {
super(); // 执行完父类构造方法(输出`Hello`),继续往下执行
// console.log(this.barStr); // Error:同上错误
console.log(Bar.barStr); // OK
}
static classMethod() {
console.log('Method Bar');
}
}

const bar = new Bar(); // => Hello; => Hello Bar
// console.log(bar.barStr); // Error: 属性“barStr”在类型“Bar”上不存在。你的意思是改为访问静态成员“Bar.barStr”吗?
console.log(Bar.barStr); // => Hello Bar
// bar.classMethod(); // Error: 属性“classMethod”在类型“Bar”上不存在。
Bar.classMethod(); // => Method Bar

抽象类

说明

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

示例

abstract class Deparment {
constructor(public name: string) {}

printName(): void {
console.log(`Department name: ${this.name}`);
}

abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Deparment {
constructor() {
super('Accounting and Auditing'); // 在派生类的构造函数中必须调用`super()`
}

printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am');
}

generateReports(): void {
console.log('Generating accounting reports...');
}
}

let department: Deparment; // OK, 允许创建一个对抽象类型的引用
// department = new Deparment(); // Error, 不能创建抽象类的实例
department = new AccountingDepartment(); // OK, 可以对抽象类的子类进行初始化和赋值
department.printName(); // OK
department.printMeeting(); // OK
// department.generateReports(); // Error, 类型“Deparment”上不存在属性“generateReports”。

使用技巧

把类当接口使用

类定义会创建两个东西:类的实例类型和一个构造函数。因为类可以创建出类型,所以也能 在允许使用接口的地方使用类

用法

  1. 作为接口
  2. 设置初始值
class Point {
x!: number;
y!: number;
}

interface Point3d extends Point {
z: number;
}

let point3d: Point3d = { x: 1, y: 2, z: 3 };

函数

说明

  1. 函数是 JS 的基础,它帮助实现抽象层,模拟类,信息隐藏和模块
  2. TS 支持类、命令空间和模块,但函数仍然是主要的定义行为的地方

函数类型

说明

  1. 函数类型包含两部分:参数类型和返回值类型
  2. 因为 TS 能够根据返回语句自动推断返回值类型,所以通常省略它
  3. 函数和返回值类型之前使用 => 符号

两种函数体

  1. 有名函数 Named function
  2. 匿名函数 Anonymouse function
// Named function
function add(x, y) {
return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y };

书写函数类型

// Named function with Function Type
function add(x: number, y: number): number {
return x + y;
}

// Anonymous function with Function Type
let myAdd: (x: number, y: number) => number = function(x: number, y: number): number { return x + y };

如果在赋值语句的一边指定了类型,另一边没有类型,TS 编译器依然会自动识别类型,这是按上下文归类,类型推论 的一种:

// Anonymous function with Function Type (Only Left)
let myAdd1 = function(x: number, y: number): number { return x + y };

// Anonymous function with Function Type (Only Right)
let myAdd2: (x: number, y: number) => number = function(x, y) { return x + y };

参数

可选参数

说明

  1. ? 定义可选参数(没传参时,值为 undefined
  2. 可选参数必须跟在参数后面

示例

function buildName(firstName: string, lastName?: string) {  // here
if (lastName) {
return `${firstName} ${lastName}`;
} else {
return firstName;
}
}
buildName('bob'); // OK
buildName('bob', undefined); // OK
buildName('bob', 'Smith'); // OK

默认参数

说明

  1. = 定义默认参数值
  2. 参数位置随意

示例

function buildName(firstName: string, lastName = 'Smith') {  // here
if (lastName) {
return `${firstName} ${lastName}`;
} else {
return firstName;
}
}
buildName('bob'); // OK
buildName('bob', undefined); // OK
buildName('bob', 'Adams'); // OK

剩余参数

说明

  1. 如果同时操作多个参数,或者不知道会有多少参数传递进来,可以使用剩余参数
  2. 在 JS 中,可以使用 arguments 来访问所有传入的参数
  3. 在 TS 中,可以把所有参数(0-任意个)收集到一个变量里,名字是 ... 后面给定的名字,编译器会创建参数数组,你可以在函数体内使用这个数组

示例

function buildName(firstName: string, ...restOfName: string[]) {
return firstName + ' ' + restOfName.join(' ');
}

let buildNameFun: (name: string, ...restName: string[]) => string = buildName;

重载

说明

  1. TS 提供函数重载的功能,用来处理 参数不同,返回值类型不同参数不同,返回值类型相同 的使用场景,使用时只需为同一个函数定义多个类型即可 4
  2. 在定义重载时,一定要把最精确的定义放在最前面(因为 TypeScript 重载的过程是,拿传入的参数和重载的方法签名列表中由上往下逐个匹配,直到找到一个完全匹配的函数签名,否则报错)

示例

declare function test(a: number): number;
declare function test(a: string): string;

const resStr = test('Hello'); // OK, resStr被推断出类型为 `string`
const resNum = test(123); // OK, resNum 被推断出类型为 `number`

// 如果写成这样,结果就不能准确推断了
// declare function test(a: number | string): number | string;

思考以下问题

interface User {
name: string;
age: number;
}

// 要求:传user时,不传flag;传number时,传入flag
declare function test(params: User | number, flag?: boolean): number;

const user = {
name: 'Jack',
age: 666
}

test(user, false); // 没报错,但不是想要的

使用函数重载

interface User {
name: string;
age: number;
}

// 要求:传user时,不传flag;传number时,传入flag
declare function test(params: User): number;
declare function test(params: number, flag: boolean): number;

const user = {
name: 'Jack',
age: 666
}

// test(user, false); // Error, 是想要的!
test(user); // OK
test(123, false); // OK

参考

Footnotes

  1. 接口 · TypeScript中文网

  2. 赋值断言 - TypeScript 2.7 · TypeScript中文网

  3. tc39/proposal-class-fields: Orthogonally-informed combination of public and private fields proposals

  4. 巧用 TypeScript (一)