TypeScript: 接口 vs. 类型别名

1,974 阅读4分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

1. 对象(Objects) / 方法(Functions)

二者均可被用来声明对象方法的签名,但语法不同。

接口

interface Point {
  x: number;
  y: number;
}

interface SetPoint {
  (x: number, y: number): void;
}

类型别名

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

type SetPoint = (x: number, y: number) => void;

2. 其他类型

接口不同,类型别名可以被用于其他类型,如基本类型、联合类型和元组。

// 基本类型
type Name = string;

// object
type PartialPointX = { x: number; };
type PartialPointY = { y: number; };

// 联合类型
type PartialPoint = PartialPointX | PartialPointY;

// 元组
type Data = [number, string];

3. 扩展(Extend)

二者均可扩展,但语法不同。另外,请注意接口类型别名并不是互斥的。接口可以扩展类型别名,反之亦然。

接口扩展接口

interface PartialPointX { x: number; }
interface Point extends PartialPointX { y: number; }

类型别名扩展类型别名

type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

接口扩展类型别名

type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

类型别名扩展接口

interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };

4. 实现(Implements)

可以以完全相同的方式实现接口类型别名。 但是请注意,接口被视为静态蓝图。 因此,他们不能实现或扩展被定义为联合类型类型别名

interface Point {
  x: number;
  y: number;
}

class SomePoint implements Point {
  x = 1;
  y = 2;
}

type Point2 = {
  x: number;
  y: number;
};

class SomePoint2 implements Point2 {
  x = 1;
  y = 2;
}

type PartialPoint = { x: number; } | { y: number; };

// FIXME: 不可以实现一个联合类型
class SomePartialPoint implements PartialPoint {
  x = 1;
  y = 2;
}

5. 声明合并

类型别名不同,接口可以被定义多次,并将被视为一个接口(合并了所有声明的成员)。

// 这里的两个定义将合并为:
// interface Point { x: number; y: number; }
interface Point { x: number; }
interface Point { y: number; }

const point: Point = { x: 1, y: 2 };

什么时候使用接口,什么时候使用类型别名?

参考资料 Abstract data type Algebraic data type Writing Easy-to-Compile Code

抽象数据类型

An abstract data type is a type with associated operations, but whose representation is hidden.

抽象数据类型是具有相关行为的类型,但其内部实现是隐藏的。

接口更适合用来定义抽象数据类型,描述中所说的行为将被定义为接口中的方法,此时只专注于定义类型的行为,而忽略其内部数据。

当然 TypeScript 的接口中是可以定义数据的,但是由于在 C# / Java 这样的完全面向对象的语言中,接口本身就被限制为只能够定义方法,而不可定义数据,所以此时使用接口是一件更自然的事情。

如下使用接口定义栈。

interface Stack<T> {
    isEmpty(): boolean
    push(value: T): number
    pop(): T
    top(): T
}

代数数据类型

This is a type where we specify the shape of each of the elements. Wikipedia has a thorough discussion. "Algebraic" refers to the property that an Algebraic Data Type is created by "algebraic" operations. The "algebra" here is "sums" and "products":

  • "sum" is alternation (A | B, meaning A or B but not both)
  • "product" is combination (A B, meaning A and B together)

在这种类型中我们指定每个元素的形状。“代数”是一个性质,指代数数据类型由“代数”操作创建。这里的“代数”是“和(sum)”和“乘积(product)”:

  • “和”代表备选(A | B,意味着 A 或者 B 但不是全部)
  • “乘积”代表组合(A B,意味着 A 和 B 一起)

类型别名就更适合定义代数数据类型,显然 TypeScript 中的“和”运算符是|,“乘积(product)”运算符是&

              5
             / \
            3   7
           / \
          1   4

下面使用类型别名描述上面给出的二叉树结构。

// 使用对象定义二叉树
type Stree<T> = undefined | {
    data: T,
    left: Stree<T>,
    right: Stree<T>
}

const stree: Stree<number> = {
    data: 5,
    left: {
        data: 3,
        left: {
            data: 1,
            left: undefined,
            right: undefined
        },
        right: {
            data: 4,
            left: undefined,
            right: undefined
        }
    },
    right: {
        data: 7,
        left: undefined,
        right: undefined
    }
}

// 使用元组定义二叉树
type Stree<T> = undefined | [Stree<T>, T, Stree<T>]

const stree: Stree<number> = [
    [
        [
            undefined,
            1,
            undefined
        ],
        3, 
        [
            undefined,
            4,
            undefined
        ]
    ],
    5,
    [
        undefined,
        7,
        undefined
    ]
]

编译性能

大多数情况下,用于声明对象类型的类型别名与接口的行为非常相似。

interface Foo { prop: string }

type Bar = { prop: string };

但是,当你需要组合两个或多个类型时,可以使用接口进行扩展,也可以使用类型别名进行交叉,此时开始存在差异。

接口创建一个对象类型,并检测属性是否冲突,解决这些冲突通常是很重要的。交叉只是递归地合并属性,在某些情况下将产生 never。接口总是展示的更好,而交叉的类型别名作为其它交叉的类型别名的一部分时不会被展示。具有最后一个值得注意的区别是,在交叉类型进行检查时,在检查交叉最终产生的类型之前,先对每个组成部分进行检查。

因此,建议使用接口/扩展来组合类型,而不是使用交叉类型。

type Foo = Bar & Baz & {
    someProp: string;
}

interface Foo extends Bar, Baz {
    someProp: string;
}