TypeScript 2.7中的Interface vs Type别名

发布于1/15/2020 来自:「前端知否」微信公众号

人们经常问我,使用类型和接口在TypeScript中定义编译时间类型有什么区别。

我以前要做的第一件事就是将它们指向TypeScript手册…

不幸的是,大多数情况下,他们找不到所需的(隐藏在“高级类型”部分中)。

现在您无需再进行任何查找,此文章是有关何时使用接口与类型别名的描述/样式指南。

官方文件说明:

“类型别名可以起到类似接口的作用,但是存在一些细微的差异。”

没错!

有什么区别?

1. “不同之处在于,接口创建了一个新名称,该名称随处可见。类型别名不会创建新名称-例如,错误消息不会使用别名。”

这是不对的! (自TypeScript 2.1起)

让我们通过接口和类型别名为Point定义编译时类型,以及getRectangleSquare函数的2种实现,它将使用接口和类型别名来进行参数类型注释。

分别定义接口Point,类型别名Point:

interface PointInterface {
x: number
y: number
}

type PointType = {
x: number
y: number
}

使用接口和类型别名的getRectangleArea函数:

const getRectangleAreaInterface = (args: PointInterface) => args.x * args.y

const getRectangleAreaAliased = (args: PointType) => args.x * args.y

因此,两者的错误相同:

// TS Error: 

// Interface:

Argument of type '{ x: number; }' is not assignable to parameter of type 'PointInterface'. Property 'y' is missing in type '{ x: number; }'.

// Type alias:

Argument of type '{ x: number; }' is not assignable to parameter of type 'PointType'. Property 'y' is missing in type '{ x: number; }'.

2. “第二个更重要的区别是类型别名不能从其扩展或实现”

这次又是不对的!

我们可以使用别名类型扩展接口:

interface ThreeDimensions extends PointType {
z: number
}

或者使用类型别名来实现Class约束

class Rectangle implements PointType {
x = 2
y = 4
}

或者使用通过类型扩展的接口来实现Class约束

class RectanglePrism implements ThreeDimensions {
x = 2
y =
z = 4
}

我们还可以结合使用类型别名和接口来实现Class约束

interface Shape {
area(): number
}

type Perimeter = {
perimiter(): number
}

class Rectangle implements PointType, Shape, Perimeter {
x = 2
y = 3

area() {
return this.x * this.y
}

perimeter() {
return 2 * (this.x + this.y)
}
}

3. “类型别名不能扩展/实现其他类型”

再一次不对!

您可以通过交集运算符&使用接口或任何其他TypeScript有效类型(具有Dictionary / JS Object的Shape,因此是非基本类型等)来进行类型别名扩展。

class Point {
x: number
y: number
}

interface Shape {
area(): number
}

type Perimeter = {
perimiter(): number
}

type RectangleShape = Shape & Perimeter & Point

class Rectangle implements RectangleShape {
x = 2
y = 3

area() {
return this.x * this.y
}

perimeter() {
return 2 * (this.x + this.y)
}
}

我们还可以将映射类型用于接口和类型别名的各种转换。 让我们通过部分映射类型将“Shape”和“Perimeter”设为可选:

class Point {
x: number
y: number
}

interface Shape {
area(): number
}

type Perimeter = {
perimiter(): number
}

type RectangleShape = Partial<Shape & Perimeter> & Point
// 和接口一样
// 接口RectangleShape 继承 Partial<Shape & Perimeter>,Point {}

class Rectangle implements RectangleShape {
x = 2
y = 3
}

弱类型检测也可以正常工作:

xxx

具有类型别名和接口的混合类型

您可能偶尔会想定义一个既具有功能又具有附加属性的对象。

我们在这里谈论的是定义一个函数的类型(可调用对象)和该函数的静态属性。

与第三方JavaScript进行交互时,也可能会看到此模式,以完全描述类型的形状。

混合类型定义和实现:

interface Counter {
// 回调函数部分
(start: number): string

// 静态属性
interval: number
reset(): void
}

const getCounter = () => {
const counter = ((start: number) => {}) as Counter
counter.interval = 123
counter.reset = () => {}
return counter
}

const callable = getCounter()

callable(10)

callable.reset()

callable.interval = 5.0

它与类型别名效果一样!

type Counter = {
// 回调函数部分
(start: number): string

// 静态属性
interval: number
reset(): void
}

但是有一个非常细微的差异。您将在IDE中获得特定的形状类型,而不是引用Counter type。

xxx

通常的好主意/做法是将混合定义分为两部分:

  • 可调用对象(函数)类型别名
/* 通过类型别名定义 */
type CounterFn = (start: number) => string

/* 通过接口定义 */
interface CounterFn {
(start: number): string
}
  • 静态特性对象形状
/* 通过类型别名定义 */
type CounterStatic = {
interval: number
reset(): void
}

/* 通过接口定义 */
interface CounterStatic {
interval: number
reset(): void
}

最终Counter 类型:

/* 通过类型别名定义 */
type Counter = CounterFn & CounterStatic

/* 通过接口定义 */
interface Counter extends CounterFn, CounterStatic {}

那么类型别名和接口又有什么区别 ?

1. 类型别名union不能用于`implements`

这将触发编译错误:

xxx

使 类型别名union 用法有意义并且可行的方式是通过字面量定义对象。因此,以下操作是有效的,并且会产生编译错误,因为我们的对象必须定义perimeter() 或area() 方法之一,或两者都定义:

xxx

2. 如果在类型定义中使用联合运算符,则不能在具有类型别名的接口上使用 extends

xxx

再次,类似于类 implements 的用法,接口是一个“静态”蓝图-它不能以一种或另一种形状存在,因此不能通过联合类型合并对其进行继承。

3. 声明合并不适用于类型别名

虽然声明合并适用于接口,但使用类型别名失败。

我所说的声明合并是什么意思:

您可以多次定义相同的接口,并且其定义将合并为一个:

interface Box {
height: number
width: number
}

interface Box {
scale: number
}

const box:Box = { height: 5, width: 6, scale: 10 }

这不适用于类型别名,因为类型是唯一的类型实体(对于全局作用域或模块作用域):

xxx

当我们为未使用TypeScript编写的库编写第三方环境类型定义时,通过接口进行的声明合并非常重要,因此,如果缺少某些定义,则消费者可以选择扩展它们。

如果我们的库是用TypeScript编写的,并且环境类型定义是自动生成的,则同样适用。

这是唯一的用例,您绝对应该始终使用接口而不是类型别名!

React Props和State使用什么?

通常,使用你想用的(类型别名/接口)只是保持一致,但就个人而言,我建议使用类型别名:

  • type Props = {} 写起来剪短一些
  • 语法一致(对于可能的类型交集,不是具有类型别名的mixin接口)
// BAD
interface Props extends OwnProps, InjectedProps, StoreProps {}
type OwnProps = {...}
type StoreProps = {...}

// GOOD
type Props = OwnProps & InjectedProps & StoreProps
type OwnProps = {...}
type StoreProps = {...}
  • 您的公共组件Props / State实现无法进行猴子修补,因此,组件的使用者永远不需要利用接口声明合并。为了扩展,有明确定义的模式,例如HOC等。

最后

在本文中,我们了解了TypeScript中的接口和类型别名之间的区别。

涵盖了这一点,我们得出了一个结论,即在特定情况下应使用哪种定义编译期类型的方法。

让我们回顾一下:

  • 类型别名可以起到类似接口的作用,但是有3个重要的区别(联合类型,声明合并)
  • 使用适合您和您的团队的方式,但要保持一致
  • 在创作库或第三方环境类型定义时,始终使用接口作为公共API的定义
  • 考虑将 type 用于您的React Component Props和State