# type 和 interface 的相同点和不同点

type 和 interface 非常相似,大部分时候,你可以任意选择使用,例如都可以描述一个对象或者函数,都允许扩展。

// Interface
// 通过继承扩展类型
interface Animal {
  name: string
}

interface Bear extends Animal {
  honey: boolean
}

const bear = getBear() 
bear.name
bear.honey
        
// Type
// 通过交集扩展类型
type Animal = {
  name: string
}

type Bear = Animal & { 
  honey: boolean 
}

const bear = getBear();
bear.name;
bear.honey;

type 和 interface 两者最关键的差别在于 type 别名本身无法添加新的属性,而 interface 是可以扩展的(这在某些场景非常有用,例如 vue 对 JSX 的扩展:https://github.com/vuejs/core/blob/main/packages/vue/jsx-runtime/index.d.ts (opens new window))。

// Interface
// 对一个已经存在的接口添加新的字段
interface Window {
  title: string
}

interface Window {
  ts: TypeScriptAPI
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
        
// Type
// 创建后不能被改变
type Window = {
  title: string
}

type Window = {
  ts: TypeScriptAPI
}

// Error: Duplicate identifier 'Window'.

在处理交叉类型的情况,两者也不相同:

interface Colorful {
  color: string;
}

interface ColorfulSub extends Colorful {
  color: number
}

// Interface 'ColorfulSub' incorrectly extends interface 'Colorful'.
// Types of property 'color' are incompatible.
// Type 'number' is not assignable to type 'string'.

使用继承的方式,如果重写类型会导致编译错误,但交叉类型不会:

type Colorful = {
  color: string;
}

type ColorfulSub = Colorful & {
  color: number
}

虽然不会报错,那 color 属性的类型是什么呢,答案是 never,取得是 string 和 number 的交集。

从语义的角度来说,interface 是接口,type 是类型,本身就是两个概念。如果你希望定义一个变量类型,就应该用 type。如果有明确的继承关系或表约束的,就用 interface。例如一个类应该是 implements 一个 interface 而不是 type。

另外,如果要写泛型的话,type 可以返回任意类型,interface 只能返回对象或函数类型,所以 type 的适用范围更广。

# infer 的协变和逆变

infer 有一个特别有用的性质,当 infer 被同一个类型变量用在多处时,infer 推导出来的类型取决于这些位置是协变还是逆变。如果位置是协变的,那么推导出的类型是各个位置分别推导的类型的 union(或关系),如果位置是逆变的,那么推导的类型是各个位置推导类型的 intersection(且关系)。

# 协变

例如下面的 U 同时处于 record 的属性里,是处于协变位置,推断出来的是并集。

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

# 逆变

而下面的例子 U 所处的位置是函数的参数,其是逆变的,推断出来的是交集。

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number(最新的 TS 会显示 never,其实就是 string & number,只是展示方式变了)

由于函数有重载的特性,当 infer 推断时参数会取交集。这种交集形成了一个重载:一个将 string 或 number 作为其参数的函数。

# 总结

在 TypeScript 中,对象、类、数组和函数的返回值类型都是协变关系,而函数的参数类型是逆变关系,所以 infer 位置如果在函数参数上,就会遵循逆变原则。

参考:Typescript infer 关键字 (opens new window)

# 设计一个泛型,接受一个对象类型,返回一个可选的 key 的对象类型,但不能是空对象

interface Todo {
  title: string
  description: string
  completed: boolean
}

type Optional<T> = {
  [key in keyof T]: {
    [newKey in key]: T[newKey]
  }
}[keyof T]

type B = Optional<Todo>;
const b: B = {} // 报错