第4章 接口与类型别名

第 4 章 接口与类型别名

4.1 type 别名

类型别名(Type Alias)是TypeScript中给类型起名字的方式。就像你给变量起名字一样,类型别名让你可以给复杂的类型起一个简短的名字,让代码更易读。


4.1.1 type 别名的基本语法

4.1.1.1 type Point = { x: number; y: number }

1
2
3
4
5
6
7
8
9
// 定义类型别名
type Point = {
    x: number;
    y: number;
};

// 使用类型别名
let location: Point = { x: 10, y: 20 };
console.log(location); // { x: 10, y: 20 }

4.1.1.2 type ID = string | number

类型别名也可以用于联合类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 简单类型别名
type ID = string | number;

let userId: ID = "abc123";
userId = 12345;  // OK,string | number
userId = true;   // 错误!boolean不是ID

// 更复杂的联合类型
type Status = "pending" | "active" | "completed" | "failed";
let currentStatus: Status = "pending";
currentStatus = "active";   // OK
currentStatus = "unknown";   // 错误!

4.1.1.3 type Callback = (error: Error | null, result: string) => void

类型别名可以用于函数类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 函数类型别名
type Callback = (error: Error | null, result: string) => void;

function fetchData(callback: Callback) {
    // 模拟异步操作
    setTimeout(() => {
        callback(null, "数据加载成功!");
    }, 1000);
}

fetchData((err, result) => {
    if (err) {
        console.error("出错了:" + err.message);
    } else {
        console.log(result); // 数据加载成功!
    }
});

4.1.2 type 别名中的泛型

4.1.2.1 泛型容器类型:type Container<T> = { value: T }

类型别名可以带泛型参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 带泛型的类型别名
type Container<T> = {
    value: T;
};

let stringContainer: Container<string> = { value: "hello" };
let numberContainer: Container<number> = { value: 42 };

console.log(stringContainer.value); // hello
console.log(numberContainer.value); // 42

4.1.2.2 可空类型:type Nullable<T> = T | null

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 更实用的泛型类型别名
type Nullable<T> = T | null;

let name: Nullable<string> = null;
name = "孙悟空";  // OK
console.log(name); // 孙悟空

let age: Nullable<number> = null;
age = 500;  // OK
console.log(age); // 500
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 更多泛型类型别名示例
type Optional<T> = T | undefined;

type Result<T> = 
    | { success: true; data: T }
    | { success: false; error: string };

// 使用
function divide(a: number, b: number): Result<number> {
    if (b === 0) {
        return { success: false, error: "除数不能为零" };
    }
    return { success: true, data: a / b };
}

const result = divide(10, 0);
if (result.success) {
    console.log("结果:" + result.data);
} else {
    console.log("错误:" + result.error); // 除数不能为零
}

4.1.3 type 别名的递归定义

4.1.3.1 type TreeNode<T> = { value: T; children: TreeNode<T>[] }

类型别名可以递归引用自己:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 树形结构的类型定义
type TreeNode<T> = {
    value: T;
    children: TreeNode<T>[];
};

type FileSystemNode = {
    name: string;
    children: FileSystemNode[];
};

// 创建一棵树
const fileSystem: FileSystemNode = {
    name: "root",
    children: [
        {
            name: "src",
            children: [
                { name: "index.ts", children: [] },
                { name: "utils.ts", children: [] }
            ]
        },
        {
            name: "package.json",
            children: []
        }
    ]
};

// 遍历树
function printTree(node: FileSystemNode, depth = 0) {
    const indent = "  ".repeat(depth);
    console.log(indent + "📁 " + node.name);
    for (const child of node.children) {
        printTree(child, depth + 1);
    }
}

printTree(fileSystem);
// 📁 root
//   📁 src
//     📁 index.ts
//     📁 utils.ts
//   📁 package.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 链表也可以用递归类型定义
type LinkedList<T> = {
    value: T;
    next: LinkedList<T> | null;
};

const list: LinkedList<number> = {
    value: 1,
    next: {
        value: 2,
        next: {
            value: 3,
            next: null
        }
    }
};

4.2 interface 声明

接口(Interface)是TypeScript中描述对象结构的主要方式。与type别名类似,但有一些独特的功能。


4.2.1 interface 的基本语法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 定义接口
interface Person {
    name: string;
    age: number;
    email?: string;  // 可选属性
}

// 使用接口
let user: Person = {
    name: "孙悟空",
    age: 500
};

console.log(user.name); // 孙悟空
console.log(user.email); // undefined —— 可选属性可以不提供

接口和type别名在基本用法上非常相似:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// type别名写法
type Person1 = {
    name: string;
    age: number;
};

// interface写法
interface Person2 {
    name: string;
    age: number;
}

// 两者几乎可以互换
let p1: Person1 = { name: "Tom", age: 20 };
let p2: Person2 = { name: "Jerry", age: 25 };

4.2.2 可选属性与只读属性

4.2.2.1 host?: string / readonly id: string

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Config {
    // 可选属性:用?标记
    host?: string;
    port?: number;
    
    // 只读属性:用readonly标记
    readonly id: string;
    readonly name: string;
}

const config: Config = {
    id: "123",
    name: "my-config"
};

// 只读属性不能修改
// config.id = "456"; // 错误!只读属性不能修改
// config.name = "new-name"; // 错误!

// 可选属性可以不提供
config.host = "localhost";
config.port = 8080;

console.log(config); // { id: '123', name: 'my-config', host: 'localhost', port: 8080 }

💡 什么时候用可选属性:当某个属性不是必须存在的时候,用?标记。比如用户的"个人网站"可能没有,那就用website?: string

💡 什么时候用只读属性:当属性创建后不应该被修改时,用readonly标记。比如订单的创建时间、唯一ID等。


4.2.3 索引签名

4.2.3.1 interface StringMap { [key: string]: string }

索引签名允许你定义对象可以有任意多个某种类型的键:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 字符串索引签名:可以用任意字符串作为键
interface StringMap {
    [key: string]: string;
}

let map: StringMap = {};
map["name"] = "孙悟空";
map["role"] = "大师兄";
map["city"] = "花果山";

console.log(map); // { name: '孙悟空', role: '大师兄', city: '花果山' }

4.2.3.2 索引签名约定了所有额外属性的类型上界:具体属性类型必须可赋值给索引签名类型;[key: string]: string | number 意味着所有动态属性只能是 string 或 number

索引签名定义了所有额外属性的类型上界——具体属性必须是可以赋值给索引签名类型的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 索引签名是string,意味着所有动态属性的值必须是string
interface StrictMap {
    [key: string]: string;
    name: string;  // OK,string可以赋值给string
    // age: number; // 错误!number不能赋值给string
}

// 如果需要支持多种类型
interface FlexibleMap {
    [key: string]: string | number;
    name: string;  // OK
    age: number;   // OK
}

let fm: FlexibleMap = {
    name: "猪八戒",
    age: 3000
};

console.log(fm.name); // 猪八戒
console.log(fm.age);  // 3000

4.2.4 方法签名的两种写法

4.2.4.1 函数属性:log: (message: string) => void

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface Logger {
    // 函数属性语法
    log: (message: string) => void;
    warn: (message: string) => void;
    error: (error: Error) => void;
}

const logger: Logger = {
    log: (msg) => console.log("[LOG] " + msg),
    warn: (msg) => console.warn("[WARN] " + msg),
    error: (err) => console.error("[ERROR] " + err.message)
};

logger.log("程序开始运行");
logger.warn("内存使用率较高");
logger.error(new Error("连接失败"));

4.2.4.2 方法语法:log(message: string): void

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
interface Logger2 {
    // 方法语法
    log(message: string): void;
    warn(message: string): void;
    error(error: Error): void;
}

const logger2: Logger2 = {
    log(message) {
        console.log("[LOG] " + message);
    },
    warn(message) {
        console.warn("[WARN] " + message);
    },
    error(error) {
        console.error("[ERROR] " + error.message);
    }
};

logger2.log("使用新Logger");

两种写法的区别:

特性函数属性方法语法
this绑定隐式,指向对象显式,指向对象本身
重写可以重写可以重写
super调用不支持支持(在类中)
代码可读性类型签名较长更简洁直观
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 函数属性 vs 方法的this区别
interface Counter {
    count: number;
    // 函数属性:this是any
    inc: (by: number) => void;
    // 方法:this是Counter
    add(by: number): void;
}

const counter: Counter = {
    count: 0,
    inc(by) {
        this.count += by; // OK,this是Counter
    },
    add(by) {
        this.count += by; // OK,this是Counter
    }
};

4.3 接口继承

接口可以像类一样继承其他接口,从而复用类型定义。


4.3.1 单接口继承、多接口继承

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 基础接口
interface Animal {
    name: string;
}

// 单继承
interface Dog extends Animal {
    breed: string;
}

let dog: Dog = {
    name: "旺财",
    breed: "金毛"
};

console.log(dog.name);  // 旺财
console.log(dog.breed); // 金毛
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 多继承
interface Walkable {
    walk(): void;
}

interface Swimmable {
    swim(): void;
}

interface Amphibian extends Walkable, Swimmable {
    habitat: string;
}

const frog: Amphibian = {
    habitat: "湿地",
    walk() {
        console.log("青蛙跳跳跳");
    },
    swim() {
        console.log("青蛙游游游");
    }
};

frog.walk(); // 青蛙跳跳跳
frog.swim(); // 青蛙游游游

4.3.2 继承中的属性覆盖规则

4.3.2.1 子类属性类型必须是父类属性类型的子类型(协变):name: stringname: 'Bob'(更具体);但反之不行

在TypeScript的结构化类型系统中,属性覆盖遵循协变规则——子类的属性类型必须是父类属性类型的子类型(更具体):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
interface Base {
    name: string;  // name是string
}

interface Derived extends Base {
    name: string;      // OK,相同类型
}

interface Derived2 extends Base {
    name: "Bob";       // OK,"Bob"是string的字面量类型,是string的子类型
}

interface Derived3 extends Base {
    name: string | null; // 错误!string | null不是string的子类型(如果strictNullChecks开启)
}

4.3.2.2 禁止完全删除父类属性:子类不能将父类属性覆盖为不存在

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface Base {
    name: string;
    age: number;
}

interface Invalid extends Base {
    name: string;
    // age被删除了?不行!
}

interface Invalid2 extends Base {
    name: string;
    age: number | undefined; // 错误!undefined是strictNullChecks开启后的情况
}

4.3.3 接口继承 vs type 交叉

接口继承和type交叉(&)都可以用来组合类型,但有一些区别:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 接口继承
interface A {
    x: string;
}
interface B {
    y: number;
}
interface AB extends A, B {
    z: boolean;
}

// type交叉
type A2 = { x: string };
type B2 = { y: number };
type AB2 = A2 & B2 & { z: boolean };

// 两者效果类似,但有细微差别
let obj1: AB = { x: "hello", y: 42, z: true };
let obj2: AB2 = { x: "hello", y: 42, z: true };

区别1:同名属性的处理

1
2
3
4
5
6
7
8
9
// type交叉:属性冲突时变成交叉类型
type T1 = { x: string };
type T2 = { x: number };
type T3 = T1 & T2; // x: never —— 矛盾!string和number不能同时成立

// interface继承:同名属性冲突时报错
interface I1 { x: string; }
interface I2 { x: number; }
// interface I3 extends I1, I2 {} // 错误!两个接口的x类型冲突

区别2:声明合并

1
2
3
4
5
6
7
// type交叉:不支持声明合并
type T = { x: string } & { x: number }; // 编译错误!不能两次定义同名属性

// interface继承:支持声明合并
interface A { x: string; }
interface A { y: number; }
const obj: A = { x: "hello", y: 42 }; // OK!两个接口合并了

4.4 类实现接口

在TypeScript中,类可以使用implements关键字来声明它必须实现某个接口。


4.4.1 implements 语法

4.4.1.1 class Dog implements Animal { name: string; breed: string }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 定义接口
interface Animal {
    name: string;
    makeSound(): void;
}

// 类实现接口
class Dog implements Animal {
    name: string;
    breed: string;

    constructor(name: string, breed: string) {
        this.name = name;
        this.breed = breed;
    }

    makeSound(): void {
        console.log(this.name + "汪汪汪!");
    }
}

class Cat implements Animal {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    makeSound(): void {
        console.log(this.name + "喵喵喵!");
    }
}

const dog = new Dog("旺财", "金毛");
const cat = new Cat("小白");

dog.makeSound(); // 旺财汪汪汪!
cat.makeSound(); // 小白喵喵喵!

4.4.2 一个类实现多个接口:class A implements B, C, D

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface Serializable {
    serialize(): string;
}

interface Cloneable<T> {
    clone(): T;
}

class User implements Serializable, Cloneable<User> {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    serialize(): string {
        return JSON.stringify({ name: this.name, age: this.age });
    }

    clone(): User {
        return new User(this.name, this.age);
    }
}

const user = new User("孙悟空", 500);
const userJson = user.serialize();
console.log(userJson); // {"name":"孙悟空","age":500}

const userClone = user.clone();
console.log(userClone.name); // 孙悟空

4.4.3 extends vs implements

关键字含义用途
extends继承类继承类,接口继承接口
implements实现类实现接口
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// extends:继承类
class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

// implements:实现接口
interface Animal {
    name: string;
}
class Dog implements Animal {
    name: string;
    breed: string;
}

// 两者结合
interface Barkable {
    bark(): void;
}

class Animal {
    name: string;
}

class Dog extends Animal implements Barkable {
    bark() {
        console.log(this.name + "汪汪汪!");
    }
}

4.5 接口合并(Declaration Merging)

TypeScript的一个独特特性是同名接口会自动合并。这在扩展第三方库类型时非常有用。


4.5.1 同名 interface 的合并规则

4.5.1.1 非函数成员直接合并,函数成员作为重载合并

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 第一次声明
interface Window {
    title: string;
}

// 第二次声明(合并)
interface Window {
    width: number;
    height: number;
}

// 合并后的Window
const myWindow: Window = {
    title: "我的窗口",
    width: 800,
    height: 600
};

console.log(myWindow.title); // 我的窗口
console.log(myWindow.width); // 800

函数成员的处理:多个同名接口中的函数会变成重载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
interface Greeter {
    greet(name: string): string;
}

interface Greeter {
    greet(name: string, time: number): string;
}

// 合并后:两个函数签名都保留(重载)
const greeter: Greeter = {
    greet(name: string, time?: number) {
        if (time !== undefined) {
            return `Good ${time > 12 ? "afternoon" : "morning"}, ${name}!`;
        }
        return `Hello, ${name}!`;
    }
};

console.log(greeter.greet("Tom"));              // Hello, Tom!
console.log(greeter.greet("Tom", 14));         // Good afternoon, Tom!

4.5.2 为什么 interface 支持声明合并

4.5.2.1 JavaScript 的对象是动态的,可以随时添加属性;interface 的声明合并模拟了 JS 的动态扩展性

JavaScript允许随时给对象添加属性:

1
2
3
// JavaScript:对象可以随时添加属性
const obj = { name: "Tom" };
obj.age = 20;  // 完全OK!

TypeScript的接口合并模拟了这个特性,让你可以分步定义一个接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 第一处定义:基础接口
interface User {
    name: string;
}

// 第二处定义:扩展接口(合并)
interface User {
    age: number;
}

// 第三处定义:再扩展
interface User {
    email?: string;
}

// 最终的User接口包含所有属性
const user: User = {
    name: "孙悟空",
    age: 500,
    email: "sunwukong@example.com"
};

4.5.2.2 应用:扩展原生 DOM 接口(window)、第三方库接口

1
2
3
4
5
6
7
8
// 扩展window接口
interface Window {
    myCustomProperty: string;
    myCustomMethod(): void;
}

window.myCustomProperty = "自定义属性";
window.myCustomMethod = () => console.log("自定义方法");
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 扩展第三方库的类型
// 假设某个库定义了
interface LibraryTypes {
    Config: { apiKey: string };
}

// 你想给Config添加新属性
interface LibraryTypes {
    Config: { apiKey: string; timeout?: number };
}

4.5.3 同名 type 不合并

1
2
3
4
5
6
// type别名:同名会报错
type Window = { title: string; };
type Window = { width: number; }; // 错误!不能重复定义

// 解决方式:用交叉类型
type Window2 = { title: string; } & { width: number; };

4.6 混合类型与接口继承类


4.6.1 混合类型:同时具有函数签名和属性签名的对象类型

混合类型是指同时具有函数特征和对象特征的类型——它既可以像函数一样被调用,又有对象的属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 混合类型:一个函数,但同时有属性
interface Counter {
    // 可以像函数一样调用
    (): number;
    // 同时有属性
    count: number;
    // 也有方法
    reset(): void;
}

function createCounter(): Counter {
    const counter = (() => {
        return ++counter.count;
    }) as Counter;
    counter.count = 0;
    counter.reset = () => {
        counter.count = 0;
    };
    return counter;
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
console.log(counter.count); // 3
counter.reset();
console.log(counter()); // 1

这个模式在JavaScript中很常见(比如jQuery),TypeScript可以用混合类型来精确描述。


4.6.2 接口继承类

TypeScript允许接口继承类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Point {
    x: number;
    y: number;
}

interface Point3D extends Point {
    z: number;
}

// Point3D自动包含了x和y,以及z
const p3d: Point3D = {
    x: 1,
    y: 2,
    z: 3
};

console.log(p3d); // { x: 1, y: 2, z: 3 }

这种用法不太常见,但在某些场景下很有用——比如当你想扩展一个类的类型定义,但不需要实现类本身时。


4.7 type 与 interface 的选用原则

这是TypeScript中最常见的问题之一:什么时候用type,什么时候用interface?


4.7.1 优先使用 interface 的场景

4.7.1.1 需要声明合并(第三方库/全局扩展);定义类实现契约(class X implements Y)时 interface 和 type 均可使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 场景1:需要声明合并
// 扩展window或第三方库类型时,必须用interface
interface Window {
    myExtension: string;
}

// 场景2:需要被类实现
// interface和type都可以被implements,但interface更直观
interface Serializable {
    serialize(): string;
}
type Serializable2 = {
    serialize(): string;
};

class A implements Serializable {}
class B implements Serializable2 {}

4.7.2 优先使用 type 的场景

4.7.2.1 需要条件类型、映射类型、元组、联合类型成员

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 场景1:联合类型
type Status = "pending" | "active" | "failed";
type StringOrNumber = string | number;

// 场景2:元组
type Pair<T, U> = [T, U];

// 场景3:条件类型
type IsString<T> = T extends string ? true : false;

// 场景4:映射类型
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// 场景5:需要复杂的交叉类型
type Base = { x: string };
type Derived = Base & { y: number };

4.7.2.2 type 更像「类型别名」的概念,适合类型运算

1
2
3
// type可以给任何类型起别名
type Callback = (error: Error | null, result: string) => void;
type Result<T> = { success: true; data: T } | { success: false; error: string };

4.7.3 项目风格一致性原则

最重要的原则是:保持一致性

如果你在一个团队中工作,跟随团队的风格。如果你是个人项目,选择一种风格并坚持使用。

一些团队的约定俗成:

flowchart TD
    A{需要定义什么?} -->|描述对象结构| C[interface]
    A -->|联合/元组/条件类型| D[type]
    A -->|需要声明合并| E[interface]
    A -->|复杂类型运算| F[type]
    
    C -->|适合用| Z[interface ✅]
    E -->|适合用| Z
    D -->|适合用| Y[type ✅]
    F -->|适合用| Y
    
    style Z fill:#3178c6,color:#fff
    style Y fill:#5cd85c,color:#fff

💡 实战经验:大多数现代TypeScript项目倾向于"接口优先"——能用interface就用interface,只有当interface不够用时才用type。比如需要联合类型、元组、复杂类型运算时,用type更合适。


📝 本节小结:type和interface在大多数情况下可以互换,但有一些区别。interface支持声明合并,适合描述对象结构和被类实现;type支持更强大的类型运算(联合类型、元组、条件类型、映射类型),适合复杂的类型操作。实际项目中建议接口优先,只在type有优势的场景使用type。


本章小结

本章学习了TypeScript中两种主要的类型定义方式:type别名和interface。

type别名使用type关键字,可以给任何类型起名字,包括原始类型、联合类型、函数类型、泛型类型,甚至可以递归定义(用于树形结构、链表)。type别名不支持声明合并。

interface使用interface关键字,专门用于描述对象的结构。interface支持可选属性(?)、只读属性(readonly)、索引签名、方法签名。interface支持声明合并——同名接口会自动合并,这个特性常用于扩展第三方库类型。

接口继承允许接口继承其他接口,子类必须保留父类属性,且子类的属性类型必须是父类属性类型的子类型(协变)。

类实现接口使用implements关键字,类必须实现接口中定义的所有成员。一个类可以实现多个接口。

混合类型是同时具有函数签名和属性签名的类型,用于描述JavaScript中常见的"函数也是对象"模式。

type vs interface的选用:大多数场景可以互换。interface适合描述对象结构和需要声明合并的场景;type适合联合类型、元组、条件类型、映射类型等类型运算场景。

下一章我们将学习联合类型、交叉类型与可辨识联合——这些是TypeScript类型系统中非常强大的组合工具。