笔者有话要讲

我们在使用 TypeScript 时,经常会用到这两个声明 interfacetype,我一直存在一个疑惑,这两个究竟有什么区别,各自又该用在什么场景。

恰巧关注的公众号——《大迁世界》推送了一篇文章——《使用 TypeScript 常见困惑:interface 和 type 的区别是什么?》,感觉文章排版有些错乱,看着属实难受。
所幸本人还是能看懂一点点英文的(指有道词典),所以干脆找到原文直接自己翻(run)译(se)好了,顺便还加了一点点补丁(patch)。


typescript-interface-and-type.jpeg

typescript 版本:4.4.4

一、概念

1.1. 类型 Types

我们知道 TypeScript 中有许多类型,有基础的类型如 string, number, 和 boolean,也有特殊的如数组 Arrays、元组 Turple 等等。这些类型使用起来非常简单,比如

1
2
const ExampleStr:string = 'my name';
const ExampleArr:number[] = [1, 2, 3];

1.2. 联合类型 Union Types

理所当然的,一个变量可能不止一种类型,这种情况在实际开发中并不少见。举个简单的例子,在一些业务中,我们会先声明一个变量,然后通过各种判断给这个变量赋值:

1
2
3
4
5
6
let target = null;
if ([some condition]) {
target = {a: 1};
} else if ([anothor condition]) {
target = {b: 2};
}

在上例中,target 的值开始时是 null 类型,程序执行完毕后是 number 类型。这种情况我们就可以使用组合类型,表明 target 可以是 null 也可以是 number。

1
2
type ObjType = { a: number };
let target: null | ObjType = null;

1.2.1. 基本类型联合

基本类型联合,允许访问任意联合成员类型,如

1
2
3
let target = string | number;
target = 'a'; // ok
target = 1; // ok

1.2.2. 对象类型联合

对象类型联合,只能访问联合中共同的成员,如

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Women {
name: string;
say(): void;
}
interface Man {
name: string,
age: number;
}
declare function Person(): Women | Man;
let him = Person();
him.name = "Jean"; // ok
him.age = 18; // error 非共同成员
him.say(); // error 非共同成员

Note: 有一个值得注意的点,如果我们使用字面量或者整体赋值的方式,而不是 obj.x=xx 的方式给变量赋值,那么赋的这个值至少应包含联合中一个成员所有属性/方法:

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
interface A {
name: string;
age: number;
}
interface B {
name: string;
gender: number;
}
// 以下三个声明都不会报错
let c: A | B = {
name: '',
gender: 1,
};

let d: A | B;
d = {
name: '',
age: 1,
};

type C = {
name: string;
age: number;
};
const c: C = {
name: '',
age: 1,
};
const d: A | B = c;

即使通过这种方式赋值,我们也不可以访问对象联合类型的非共有属性/方法:

1
2
3
4
5
6
7
8
let inst: A | B = {
name: '',
age: 1;
gender: 1,
};
console.log(inst); // { name: '', age: 1, gender: 1 }
inst.age = 2; // 类型“A | B”上不存在属性“age”。
inst.gender = 0; // 类型“A | B”上不存在属性“gender”。

1.3. 交叉类型 Intersection Types

与联合类型取交集不同,交叉类型取的并集,即,交叉类型允许访问成员类型的所有属性。

自然,交叉类型只能是两个对象类型的交叉,毕竟基本类型也没办法交叉不是。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Women {
name: string;
say(): void;
}
interface Man {
name: string,
age: number;
}
declare function Person(): Women & Man;
let him = Person();
him.name = "Jean"; // ok
him.age = 18; // ok
him.say(); // ok

交叉类型对象的赋值必须包含该交叉类型指定的所有属性/方法。

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

const c: A & B = {
name: '',
age: 1,
};
// 不能将类型“{ name: string; age: number; }”分配给类型“A & B”。
// 类型 "{ name: string; age: number; }" 中缺少属性 "gender",但类型 "B" 中需要该属性。

1.4. 类型别名 Type Aliases

举个栗子

1
2
3
const arr1: [string, number] = ['a', 1];
const obj1: { a: string, b: number } = { a: 'a', b: 2 };
const prim1: string | number | boolean = 'a';

在实际的开发过程中,我们或多或少会遇到上面这种写法才能实现的逻辑。这种变量只有一个两个的话还好,如果有大量相同类型的变量,比如

1
2
3
4
const obj1: { a: string, b: number } = { a: 'a', b: 2 };
const obj2: { a: string, b: number } = { a: 'a', b: 2 };
const obj3: { a: string, b: number } = { a: 'a', b: 2 };
...

也许写起来还不会太麻烦(ctrl+c/ctrl+v 就好),但是看上去令人强迫症(指“优化”代码)发作。

TypeScript 自然也提供了解决方案,那就是 类型别名 Type Aliases

1
2
3
4
type CommonType = { a: string, b: number };
const obj1: CommonType = { a: 'a', b: 2 };
const obj2: CommonType = { a: 'a', b: 2 };
const obj3: CommonType = { a: 'a', b: 2 };

类型别名允许我们通过一个名称来使用一些或简单(正常会用在基本类型上么?)或复杂的类型,这样无论是编写难易度还是代码美观程度上,都比之前那种写法好得多。

1.5. 接口 Interfaces

官方文档中的解释:An interface declaration is another way to name an object type。interface 是声明对象类型的一种方式。

举个栗子

1
2
3
4
5
6
type CommonType = { a: string, b: number };
// 也可以写成
interface CommonType {
a: string,
b: number,
}

二、相同点 type and interface

  1. 都可以声明对象类型;

    1
    2
    3
    4
    5
    6
    7
    8
    interface PointA {
    x: number;
    y: number;
    }
    type PointB = {
    x: number;
    y: number;
    }
  2. 都可以被实现(implements);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    interface InterfaceA {
    name: string;
    };
    class PersonA implements InterfaceA {
    name = 'Kaze';
    }

    type TypeB = { age: number };
    class PersonB implements TypeB {
    age = 17;
    }
  3. 都可以联合、交叉

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    type A = {
    name: string;
    age: number;
    }
    type B = {
    name: string;
    isMale: boolean;
    }
    let Person1: A | B;
    let Person2: A & B;
    // interface 也可以
    interface A {
    name: string;
    age: number;
    }
    interface B {
    name: string;
    isMale: boolean;
    }
    let Person1: A | B;
    let Person2: A & B;

三、不同点 type vs interface

  1. 声明语法不同

    1
    2
    3
    4
    5
    6
    7
    8
    type A = { name: string };
    type B = [string, number];
    type C = string;

    interface D {
    name: string;
    age: number;
    }
  2. 重复声明效果不同

    1. interface 重复声明会合并,同名替换

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      interface Person {
      name: string;
      gender: string;
      }
      interface Person {
      age: number;
      gender: number;
      }
      // 等同于
      interface Person {
      name: string;
      age: number;
      gender: number;
      }
    2. type 重复声明会报错

      1
      2
      3
      4
      5
      6
      7
      type One = {
      name: string;
      }
      type One = {
      age: number;
      }
      // 报错:Duplicate identifier "One"

interface 只针对对象类型,而 type 则没有此限制(毕竟只是个别名而已)。举个栗子

1
2
3
4
5
6
7
8
9
10
11
type ArrType = [string, number];
// interface 是不能直接声明数组的
interface ArrType [string, number]; // 语法错误

// 当然,你可以通过嵌套方式实现该效果,但。。。
interface ObjType {
arr: [string, number]
}
const obj:ObjType = {
arr: ['a', 1]
}

你甚至可以

1
2
3
4
5
interface Person {
name: string;
gender: string;
}
type One = Person;

interface 和 type 都可以实现继承,甚至 interface 和 type 之间也可以相互继承,但语法有所区别。

  1. Interface 继承 interface:使用 extends 关键字

    1
    2
    3
    4
    5
    6
    interface A {
    name: string;
    }
    interface B extends A {
    age: number;
    }
  2. interface 继承 type:同样使用 extends 关键字

    1
    2
    3
    4
    5
    6
    type A = {
    name: string;
    }
    interface B extends A {
    age: number;
    }
  3. type 继承 type:交叉类型

    1
    2
    3
    4
    5
    6
    type A = {
    name: string;
    }
    type B = A & {
    age: number;
    }
  4. type 继承 interface:交叉类型

    1
    2
    3
    4
    5
    6
    interface A {
    name: string;
    }
    type B = A & {
    age: number;
    }

如何使用

在大多数情况下,你可以根据个人偏好进行选择,TypeScript 会告诉你是否需要其他类型的声明。[3]

如果您想使用启发式方法(heuristic),除部分需要使用 type 特性的情况外,请使用接口 interface。[3]

对于库中的公共 API 定义或者第三方类型定义,应使用接口(声明合并功能)。

除此之外,我们可以随意使用,但在整个项目中应保持一致性

这就是 TypeScript 中关于接口 vs 类型的所有知识。 希望这篇文章能帮到你,如果对你有所帮助的话,请分享给你的朋友!


参考文献:

[1] TypeScript 官方文档#type-aliases

[2] TypeScript 官方文档#interfaces

[3] TypeScript 官方文档#Differences Between Type Aliases and Interfaces

[4] 使用 TypeScript 常见困惑:interface 和 type 的区别是什么?

[5] [SARANSH KATARIA]TypeScript: the difference between interface and type