TypeScript 中的接口与类型

Interfaces vs Types in TypeScript

TypeScript 中的这些语句(interface vs type)有什么区别?

interface X {
    a: number
    b: string
}

type X = {
    a: number
    b: string
};

2021 年 3 月更新:较新的 TypeScript 手册(也在 nju-clc 中提到 下面的答案 ) 有一节 Interfaces vs. Type Aliases 解释了差异。


原始答案 (2016)

根据 (now archived) TypeScript Language Specification:

Unlike an interface declaration, which always introduces a named object type, a type alias declaration can introduce a name for any kind of type, including primitive, union, and intersection types.

规范接着提到:

Interface types have many similarities to type aliases for object type literals, but since interface types offer more capabilities they are generally preferred to type aliases. For example, the interface type

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

could be written as the type alias

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

However, doing so means the following capabilities are lost:

  • An interface can be named in an extends or implements clause, but a type alias for an object type literal cannot No longer true since TS 2.7.
  • An interface can have multiple merged declarations, but a type alias for an object type literal cannot.

https://www.typescriptlang.org/docs/handbook/advanced-types.html

One difference is that interfaces create a new name that is used everywhere. Type aliases don’t create a new name — for instance, error messages won’t use the alias name.

2019 年更新


当前答案和 official documentation 已过时。对于那些刚接触 TypeScript 的人来说,如果没有示例,所使用的术语就不清楚了。以下是最新差异列表。

1。对象/函数

两者都可以用来描述物体的形状或函数签名。但语法不同。

界面

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。其他类型

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

// primitive
type Name = string;

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

// union
type PartialPoint = PartialPointX | PartialPointY;

// tuple
type Data = [number, string];

3。扩展

两者都可以扩展,但同样,语法不同。此外,请注意接口和类型别名并不相互排斥。接口可以扩展类型别名,反之亦然。

接口扩展接口

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。工具

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

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: can not implement a union type
class SomePartialPoint implements PartialPoint {
  x = 1;
  y = 2;
}

5。声明合并

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

// These two declarations become:
// interface Point { x: number; y: number; }
interface Point { x: number; }
interface Point { y: number; }

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

自 TypeScript 3.2(2018 年 11 月)起,以下内容为真:

Aspect Type Interface
Can describe functions
Can describe constructors
Can describe tuples
Interfaces can extend it ⚠️
Classes can extend it
Classes can implement it (implements) ⚠️
Can intersect another one of its kind ⚠️
Can create a union with another one of its kind
Can be used to create mapped types
Can be mapped over with mapped types
Expands in error messages and logs
Can be augmented
Can be recursive ⚠️

⚠️在某些情况下

类型示例:

// 为对象创建树结构。由于缺少交集 (&)

,您不能对界面执行相同的操作
type Tree<T> = T & { parent: Tree<T> };

// 类型限制一个变量只能赋几个值。接口没有联合 (|)

type Choise = "A" | "B" | "C";

// 多亏了类型,你可以通过条件机制声明 NonNullable 类型。

type NonNullable<T> = T extends null | undefined ? never : T;

接口示例:

// 你可以使用面向对象的接口并使用 'implements' 来定义 object/class 骨架

interface IUser {
    user: string;
    password: string;
    login: (user: string, password: string) => boolean;
}

class User implements IUser {
    user = "user1"
    password = "password1"

    login(user: string, password: string) {
        return (user == user && password == password)
    }
}

// 你可以用其他接口扩展接口

    interface IMyObject {
        label: string,
    }

    interface IMyObjectWithSize extends IMyObject{
        size?: number
    }

文档已经解释

  • One difference is that interfaces create a new name that is used everywhere. Type aliases don’t create a new name — for instance, error messages won’t use the alias name.in older versions of TypeScript, type aliases couldn’t be extended or implemented from (nor could they extend/implement other types). As of version 2.7, type aliases can be extended by creating a new intersection type
  • On the other hand, if you can’t express some shape with an interface and you need to use a union or tuple type, type aliases are usually the way to go.

Interfaces vs. Type Aliases

除了已经提供的出色答案之外,在扩展 类型与接口方面存在明显差异。我最近 运行 遇到了几个接口无法完成工作的案例:

  1. Couldn't extend a union type using an interface
  2. Couldn't extend generic interface

其他答案都很棒! Type 可以做但 Interface 不能

的其他一些事情

你可以在类型中使用 union

type Name = string | { FullName: string };

const myName = "Jon"; // works fine

const myFullName: Name = {
  FullName: "Jon Doe", //also works fine
};

遍历类型中的联合属性

type Keys = "firstName" | "lastName";

type Name = {
  [key in Keys]: string;
};

const myName: Name = {
  firstName: "jon",
  lastName: "doe",
};

交集类型(但是,在与 extends 的接口中也支持)

type Name = {
  firstName: string;
  lastName: string;
};

type Address = {
  city: string;
};

const person: Name & Address = {
  firstName: "jon",
  lastName: "doe",
  city: "scranton",
};

也不是说 typeinterface 晚引入,根据 TS 的最新版本 type 几乎可以做 interface 可以做的所有事情并且多得多!


*ex​​cept Declaration merging个人意见:类型不支持还好,可能会导致代码不一致

接口与类型

接口和类型用于描述对象和基元的类型。接口和类型通常可以互换使用,并且通常提供相似的功能。通常是程序员自己挑自己喜欢的选择。

但是,接口只能描述对象和创建这些对象的类。因此必须使用类型来描述像字符串和数字这样的基元。

以下是接口和类型之间的 2 个差异的示例:

// 1. Declaration merging (interface only)

// This is an extern dependency which we import an object of
interface externDependency { x: number, y: number; }
// When we import it, we might want to extend the interface, e.g. z:number
// We can use declaration merging to define the interface multiple times
// The declarations will be merged and become a single interface
interface externDependency { z: number; }
const dependency: externDependency = {x:1, y:2, z:3}

// 2. union types with primitives (type only)

type foo = {x:number}
type bar = { y: number }
type baz = string | boolean;

type foobarbaz = foo | bar | baz; // either foo, bar, or baz type

// instances of type foobarbaz can be objects (foo, bar) or primitives (baz)
const instance1: foobarbaz = {y:1} 
const instance2: foobarbaz = {x:1} 
const instance3: foobarbaz = true 

何时使用 type


通用转换

在将多个类型转换为单个泛型类型时使用 type

示例:

type Nullable<T> = T | null | undefined
type NonNull<T> = T extends (null | undefined) ? never : T

类型别名

我们可以使用 type 为难以阅读且不便重复输入的长或复杂类型创建别名。

示例:

type Primitive = number | string | boolean | null | undefined

像这样创建别名可以使代码更加简洁和可读。


类型捕获

使用 type 在类型未知时捕获对象的类型。

示例:

const orange = { color: "Orange", vitamin: "C"}
type Fruit = typeof orange
let apple: Fruit

在这里,我们得到未知类型的orange,将其命名为Fruit,然后使用Fruit创建一个新的type-safe对象apple.


何时使用 interface


多态

interface 是实现数据形状的合约。使用接口明确表明它旨在作为对象使用方式的约定来实现和使用。

示例:

interface Bird {
    size: number
    fly(): void
    sleep(): void
}

class Hummingbird implements Bird { ... }
class Bellbird implements Bird { ... }

虽然您可以使用 type 来实现这一点,但 Typescript 更多地被视为一种面向对象的语言,而 interface 在面向对象的语言中占有特殊的地位。当您在团队环境中工作或为开源社区做贡献时,使用 interface 可以更轻松地阅读代码。对于来自其他面向对象语言的新程序员来说也很容易。

官方 Typescript documentation 还说:

... we recommend using an interface over a type alias when possible.

这也表明 type 更倾向于创建类型别名,而不是创建类型本身。


声明合并

您可以使用 interface 的声明合并功能向已声明的 interface 添加新的属性和方法。这对于第三方库的环境类型声明很有用。当第三方库缺少某些声明时,您可以使用相同的名称重新声明接口并添加新的属性和方法。

示例:

我们可以扩展上面的 Bird 接口以包含新的声明。

interface Bird {
    color: string
    eat(): void
}

就是这样!记住何时使用什么比迷失在两者之间的细微差别中更容易。

好吧 'typescriptlang' 似乎建议尽可能使用接口而不是类型。 Interface vs Type Alias

还有一个区别。如果你能解释这种情况的原因,我会...请你喝啤酒:

enum Foo { a = 'a', b = 'b' }

type TFoo = {
  [k in Foo]: boolean;
}

const foo1: TFoo = { a: true, b: false} // good
// const foo2: TFoo = { a: true }       // bad: missing b
// const foo3: TFoo = { a: true, b: 0}  // bad: b is not a boolean

// So type does roughly what I'd expect and want

interface IFoo {
//  [k in Foo]: boolean;
/*
  Uncommenting the above line gives the following errors:
  A computed property name in an interface must refer to an expression whose type is a      
    literal type or a 'unique symbol' type.
  A computed property name must be of type 'string', 'number', 'symbol', or 'any'.
  Cannot find name 'k'.
*/
}

// ???

这让我想说 接口见鬼去吧 除非我有意实现一些 OOP 设计模式,或者需要如上所述的合并(我从来没有除非我有一个非常好的理由。

索引的不同。

interface MyInterface {
  foobar: string;
}

type MyType = {
  foobar: string;
}

const exampleInterface: MyInterface = { foobar: 'hello world' };
const exampleType: MyType = { foobar: 'hello world' };

let record: Record<string, string> = {};

record = exampleType;      // Compiles
record = exampleInterface; // Index signature is missing

Related issue: Index signature is missing in type (only on interfaces, not on type alias)

所以请考虑这个例子,如果你想索引你的对象

看看这个 and 违反里氏原则

评价差异

FirstLevelType为interface

时查看ExtendFirst的结果类型
/**
 * When FirstLevelType is interface 
 */

interface FirstLevelType<A, Z> {
    _: "typeCheck";
};

type TestWrapperType<T, U> = FirstLevelType<T, U>;


const a: TestWrapperType<{ cat: string }, { dog: number }> = {
  _: "typeCheck",
};

// {  cat: string; }
type ExtendFirst = typeof a extends FirstLevelType<infer T, infer _>
    ? T
    : "not extended";

FirstLevelType是一个类型时,查看ExtendFirst的结果类型:


/**
 * When FirstLevelType is type
 */
type FirstLevelType<A, Z>= {
    _: "typeCheck";
};

type TestWrapperType<T, U> = FirstLevelType<T, U>;


const a: TestWrapperType<{ cat: string }, { dog: number }> = {
  _: "typeCheck",
};

// unknown
type ExtendFirst = typeof a extends FirstLevelType<infer T, infer _>
    ? T
    : "not extended";

就编译速度而言,组合接口的性能优于类型交集:

[...] interfaces create a single flat object type that detects property conflicts. This is in contrast with intersection types, where every constituent is checked before checking against the effective type. Type relationships between interfaces are also cached, as opposed to intersection types.

来源:https://github.com/microsoft/TypeScript/wiki/Performance#preferring-interfaces-over-intersections

TypeScript handbook给出答案:

Almost all features of an interface are available in type.
The key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

2021 年相关

TypeScript 版本:4.3.4


TLDR;

我在下面描述的个人惯例是这样的:

Always prefer interface over type.

何时使用type:

  • 为基本类型(字符串、布尔值、数字、bigint、符号等)定义别名时使用type
  • 定义元组类型时使用type
  • 定义函数类型时使用type
  • 定义联合时使用type
  • 尝试通过组合重载对象类型中的函数时使用type
  • 在需要利用映射类型时使用 type

何时使用interface:

  • 对不需要使用 type 的所有对象类型使用 interface(见上文)
  • 如果您想利用声明合并,请使用 interface

原始类型

typeinterface 之间最容易看出的区别是只有 type 可用于为基元设置别名:

type Nullish = null | undefined;
type Fruit = 'apple' | 'pear' | 'orange';
type Num = number | bigint;

None 这些示例可以通过接口实现。

为原始值提供类型别名时,请使用 type 关键字。

元组类型

元组只能通过 type 关键字输入:

type row = [colOne: number, colTwo: string];

为元组提供类型时使用 type 关键字。

函数类型

函数可以通过 typeinterface 关键字输入:

// via type
type Sum = (x: number, y: number) => number;

// via interface
interface Sum {
  (x: number, y: number): number;
}

由于两种方式都可以达到相同的效果,因此在这些情况下规则将使用 type,因为它更容易阅读(并且不那么冗长)。

定义函数类型时使用type

联合类型

联合类型只能通过type关键字实现:

type Fruit = 'apple' | 'pear' | 'orange';
type Vegetable = 'broccoli' | 'carrot' | 'lettuce';

// 'apple' | 'pear' | 'orange' | 'broccoli' | 'carrot' | 'lettuce';
type HealthyFoods = Fruit | Vegetable;

定义联合类型时,使用type关键字

对象类型

JavaScript 中的对象是 key/value 地图,“对象类型”是打字稿输入这些 key/value 地图的方式。 interfacetype 都可以在为对象提供类型时使用,因为原始问题很清楚。那么什么时候对对象类型使用 typeinterface

交集与继承

有了类型和组合,我可以做这样的事情:

interface NumLogger { 
    log: (val: number) => void;
}
type StrAndNumLogger = NumLogger & { 
  log: (val: string) => void;
}

const logger: StrAndNumLogger = {
  log: (val: string | number) => console.log(val)
}

logger.log(1)
logger.log('hi')

Typescript 非常开心。如果我尝试使用接口扩展它怎么办:


interface StrAndNumLogger extends NumLogger { 
    log: (val: string) => void; 
};

StrAndNumLogger 的声明给了我一个 error:

Interface 'StrAndNumLogger' incorrectly extends interface 'NumLogger'

对于接口,子类型必须与超类型中声明的类型完全匹配,否则 TS 将抛出上述错误。

尝试重载对象类型的函数时,最好使用 type 关键字。

声明合并

打字稿中接口与类型的主要区别在于它们在声明后可以使用新功能进行扩展。当您想要扩展从节点模块导出的类型时,会出现此功能的一个常见用例。例如,@types/jest 导出可以在使用 jest 库时使用的类型。但是,jest 还允许使用新功能扩展主要 jest 类型。例如,我可以像这样添加自定义测试:

jest.timedTest = async (testName, wrappedTest, timeout) =>
  test(
    testName,
    async () => {
      const start = Date.now();
      await wrappedTest(mockTrack);
      const end = Date.now();

      console.log(`elapsed time in ms: ${end - start}`);
    },
    timeout
  );

然后我可以这样使用它:

test.timedTest('this is my custom test', () => {
  expect(true).toBe(true);
});

现在,一旦测试完成,该测试经过的时间将打印到控制台。伟大的!只有一个问题——打字稿不知道我添加了一个 timedTest 函数,所以它会在编辑器中抛出一个错误(代码会 运行 正常,但 TS 会生气)。

为了解决这个问题,我需要告诉 TS 在 jest 已经可用的现有类型之上有一个新类型。为此,我可以这样做:

declare namespace jest {
  interface It {
    timedTest: (name: string, fn: (mockTrack: Mock) => any, timeout?: number) => void;
  }
}

由于接口的工作方式,此类型声明将合并 与从@types/jest 导出的类型声明。所以我不只是重新声明 jest.It;我用一个新函数扩展了 jest.It,以便 TS 现在知道我的自定义测试函数。

type 关键字无法实现这种类型的内容。如果 @types/jest 使用 type 关键字声明了它们的类型,我将无法使用我自己的自定义类型来扩展这些类型,因此也就没有让 TS 满意的好方法我的新功能。 interface 关键字独有的这个过程称为 declaration merging.

声明合并也可以像这样在本地进行:

interface Person {
  name: string;
}

interface Person {
  age: number;
}

// no error
const person: Person = {
  name: 'Mark',
  age: 25
};

如果我用 type 关键字做同样的事情,我会得到一个错误,因为类型不能是 re-declared/merged。在现实世界中,JavaScript 对象很像这个 interface 示例;它们可以在 运行 时间用新字段动态更新。

因为接口声明可以合并,所以接口比类型更准确地表示 JavaScript 对象的动态特性,因此应该首选它们。

映射对象类型

使用 type 关键字,我可以像这样利用 mapped types

type Fruit = 'apple' | 'orange' | 'banana';

type FruitCount = {
  [key in Fruit]: number;
}

const fruits: FruitCount = {
  apple: 2,
  orange: 3,
  banana: 4
};

这不能用接口来完成:

type Fruit = 'apple' | 'orange' | 'banana';

// ERROR: 
interface FruitCount {
  [key in Fruit]: number;
}

当需要利用映射类型时,使用 type 关键字

性能

很多时候,对象类型的简单类型别名与接口非常相似。

interface Foo { prop: string }

type Bar = { prop: string };

但是,一旦您需要组合两种或多种类型,您就可以选择使用接口扩展这些类型,或者在类型别名中将它们相交,这就是差异开始变得重要的时候。

接口创建一个单一的平面对象类型来检测 属性 冲突,这通常很重要,需要解决!另一方面,交叉点只是递归地合并属性,在某些情况下永远不会产生。界面也始终显示得更好,而交叉点的类型别名不能在其他交叉点的一部分中显示。接口之间的类型关系也被缓存,而不是作为一个整体的交集类型。最后一个值得注意的区别是,在检查目标交叉点类型时,在检查“有效”/“扁平化”类型之前检查每个成分。

因此,建议使用 interfaces/extends 扩展类型而不是创建交集类型。

更多关于 typescript wiki

documentation 中指出的主要区别是 Interface 可以重新打开以添加新的 属性 但 Type alias 不能重新打开以添加新的 [=24] =] 例如:

没关系

interface x {
  name: string
}

interface x {
  age: number
}

这会抛出错误 Duplicate identifier y

type y = {
  name: string
}

type y = {
  age: number
}

除此之外,接口和类型别名相似。

语义不同

接口是 TS 类型系统中的常规语法元素。它是 TS 语法的原生部分。

而类型别名是语法糖。这是一种元编程。

在打字稿中,建议使用“接口”而不是“类型”。

  • “类型”用于创建类型别名。

     type Data=string
    

那么您可以使用“Data”代替字符串

const name:string="Yilmaz"
const name:Data="Yilmaz"

别名非常有用,尤其是在处理泛型类型时。

您不能使用“接口”执行此操作。

  • 您可以合并接口但不能合并类型。

    接口人{ 名称:字符串; }

      interface Person {
        age: number;
      }
    
      const me: Person = {
        name: "Yilmaz Bingol",
        age: 30
      };
    
  • 函数式编程用户选择“类型”,面向对象编程用户选择“接口”

  • 你不能在接口上有计算或计算属性,但在类型上。

    键入全名 = "姓名" | “姓氏”

      type  Person= {
         [key in Keys]: string
      }
    
      const me: Person = {
         firstname: "Yilmaz",
         lastname: "Bingol"
      }
    

展示以递归方式而不是 class members/properties/functions.

递归重写对象文字类型和接口的能力

还有如何区分和类型检查差异以及解决上述问题的方法,当 Record 由于是接口和类似的东西而不起作用时,您可以解决它。 这将允许简化猫鼬类型的以下内容: https://github.com/wesleyolis/mongooseRelationalTypes mongooseRelationalTypes, DeepPopulate, populate

此外,还有许多其他方法来执行高级类型泛型和类型推断以及围绕它提高速度的怪癖,所有这些小技巧都是通过多次试验和试错来使它们正确的。

打字稿游乐场: Click here for all examples in a live play ground

    class TestC {
        constructor(public a: number, public b: string, private c: string) {
    
        }
    }
    
    class TestD implements Record<any, any> {
    
        constructor(public a: number, public b: string, private c: string) {
    
        }
    
        test() : number {
            return 1;
        }
    }
    
    type InterfaceA = {
         a: string,
        b: number,
        c: Date
        e: TestC,
        f: TestD,
        p: [number],
        neastedA: {
            d: string,
            e: number
            h: Date,
            j: TestC
            neastedB: {
                d: string,
                e: number
                h: Date,
                j: TestC
            }
        }
    }


    type TCheckClassResult = InterfaceA extends Record<any, unknown> ? 'Y': 'N' // Y

    const d = new Date();
    type TCheckClassResultClass = typeof d extends Record<any, unknown> ? 'Y': 'N'      // N

    const metaData = Symbol('metaData');
    type MetaDataSymbol = typeof metaData;

    // Allows us to not recuse into class type interfaces or traditional interfaces, in which properties and functions become optional.
    type MakeErrorStructure<T extends Record<any, any>> = {
        [K in keyof T] ?: (T[K] extends Record<any, unknown> ?         MakeErrorStructure<T[K]>: T[K] & Record<MetaDataSymbol, 'customField'>)
    }

    type MakeOptional<T extends Record<any, any>> = {
        [K in keyof T] ?: T[K] extends Record<any, unknown> ? MakeOptional<T[K]> : T[K]
    }

    type RRR = MakeOptional<InterfaceA>
    const res  = {} as RRR;

    const num = res.e!.a; // type == number
    const num2 = res.f!.test(); // type == number

使递归形状或特定形状的键递归

    type MakeRecusive<Keys extends string, T> = {
        [K in Keys]: T & MakeRecusive<K, T>
      } & T
  
    type MakeRecusiveObectKeys<TKeys extends string, T> = {
        [K in keyof T]: K extends TKeys ? T[K] & MakeRecusive<K, T[K]>: T[K]
    }

如何为记录类型应用类型约束,它可以验证像鉴别器这样的接口:

    type IRecordITypes = string | symbol | number;
    
    // Used for checking interface, because Record<'key', Value> excludeds interfaces
    type IRecord<TKey extends IRecordITypes, TValue> = {
        [K in TKey as `${K & string}`] : TValue
    } 


    // relaxies the valiation, older versions can't validate.
    // type IRecord<TKey extends IRecordITypes, TValue> = {
    //     [index: TKey] : TValue
    // } 

    
    type IRecordAnyValue<T extends Record<any,any>, TValue> = {
        [K in keyof T] : TValue
    }    

    interface AA {
        A : number,
        B : string
    }

    interface BB {
        A: number,
        D: Date
    }

    // This approach can also be used, for indefinitely recursive validation like a deep populate, which can't determine what validate beforehand.
    interface CheckRecConstraints<T extends IRecordAnyValue<T, number | string>> {
    }

    type ResA = CheckRecConstraints<AA> // valid

    type ResB = CheckRecConstraints<BB> // invalid

Alternative for checking keys:

    type IRecordKeyValue<T extends Record<any,any>, TKey extends IRecordITypes, TValue> = 
    {
        [K in keyof T] : (TKey & K) extends never ? never : TValue
    } 
    
    // This approach can also be used, for indefinitely recursive validation like a deep populate, which can't determine what validate beforehand.
    interface CheckRecConstraints<T extends IRecordKeyValue<T, number | string, number | string>> {
        A : T
    }

    type UUU = IRecordKeyValue<AA, string, string | number>

    type ResA = CheckRecConstraints<AA> // valid

    type ResB = CheckRecConstraints<BB> // invalid

使用鉴别器的示例,但是,为了速度,我宁愿使用字面意思,它定义了记录的每个键,然后传递给生成混合值,因为使用更少的内存并且比这种方法更快。


    type EventShapes<TKind extends string> = IRecord<TKind, IRecordITypes> | (IRecord<TKind, IRecordITypes> & EventShapeArgs)

    type NonClassInstance = Record<any, unknown>
    type CheckIfClassInstance<TCheck, TY, TN> = TCheck extends NonClassInstance ? 'N' : 'Y'

    type EventEmitterConfig<TKind extends string = string, TEvents extends EventShapes<TKind> = EventShapes<TKind>, TNever = never> = {
        kind: TKind
        events: TEvents
        noEvent: TNever
    }

    type UnionDiscriminatorType<TKind extends string, T extends Record<TKind, any>> = T[TKind]

    type PickDiscriminatorType<TConfig extends EventEmitterConfig<any, any, any>,
        TKindValue extends string,
        TKind extends string = TConfig['kind'],        
        T extends Record<TKind, IRecordITypes> & ({} | EventShapeArgs) = TConfig['events'],
        TNever = TConfig['noEvent']> = 
            T[TKind] extends TKindValue 
            ? TNever
            : T extends IRecord<TKind, TKindValue>
                ? T extends EventShapeArgs
                    ? T['TArgs']
                    : [T]
                : TNever        

    type EventEmitterDConfig = EventEmitterConfig<'kind', {kind: string | symbol}, any>
    type EventEmitterDConfigKeys = EventEmitterConfig<any, any> // Overide the cached process of the keys.

    interface EventEmitter<TConfig extends EventEmitterConfig<any, any, any> = EventEmitterDConfig,
                TCacheEventKinds extends string = UnionDiscriminatorType<TConfig['kind'], TConfig['events']>
                > {
      on<TKey extends TCacheEventKinds, 
                    T extends Array<any> = PickDiscriminatorType<TConfig, TKey>>(
                        event: TKey, 
                        listener: (...args: T) => void): this;

     emit<TKey extends TCacheEventKinds>(event: TKey, args: PickDiscriminatorType<TConfig, TKey>): boolean;
    }

用法示例:

    interface EventA {
        KindT:'KindTA'
        EventA: 'EventA'
    }

    interface EventB {
        KindT:'KindTB'
        EventB: 'EventB'
    }

    interface EventC {
        KindT:'KindTC'
        EventC: 'EventC'
    }

    interface EventArgs {
        KindT:1
        TArgs: [string, number]    
    }
    const test :EventEmitter<EventEmitterConfig<'KindT', EventA | EventB | EventC | EventArgs>>;

    test.on("KindTC",(a, pre) => {
        
    })

更好的方法来区分类型和从地图中选择类型以缩小范围,这通常会导致更快的性能和更少的类型操作开销,并允许改进缓存。与上面的示例进行比较。


    type IRecordKeyValue<T extends Record<any,any>, TKey extends IRecordITypes, TValue> = 
    {
        [K in keyof T] : (TKey & K) extends never ? never : TValue
    } 

    type IRecordKeyRecord<T extends Record<any,any>, TKey extends IRecordITypes> = 
    {
        [K in keyof T] : (TKey & K) extends never ? never : T[K] // need to figure out the constrint here for both interface and records.
    } 
    
    type EventEmitterConfig<TKey extends string | symbol | number, TValue, TMap extends IRecordKeyValue<TMap, TKey, TValue>> = {
        map: TMap
    }

    type PickKey<T extends Record<any,any>, TKey extends any> = (T[TKey] extends Array<any> ? T[TKey] : [T[TKey]]) & Array<never>

    type EventEmitterDConfig = EventEmitterConfig<string | symbol, any, any>


    interface TDEventEmitter<TConfig extends EventEmitterConfig<any, any, TConfig['map']> = EventEmitterDConfig,
        TMap = TConfig['map'],
        TCacheEventKinds = keyof TMap
    > {
        
        on<TKey extends TCacheEventKinds, T extends Array<any> = PickKey<TMap, TKey>>(event: TKey, 
            listener: (...args: T) => void): this;

        emit<TKey extends TCacheEventKinds, T extends Array<any> = PickKey<TMap, TKey>>(event: TKey, ...args: T): boolean;
    }
   
    type RecordToDiscriminateKindCache<TKindType extends string | symbol | number, TKindName extends TKindType, T extends IRecordKeyRecord<T, TKindType>> = {
        [K in keyof T] : (T[K] & Record<TKindName, K>)
    }

    type DiscriminateKindFromCache<T extends IRecordKeyRecord<T, any>> = T[keyof T]

用法示例:

    
    interface EventA {
        KindT:'KindTA'
        EventA: 'EventA'
    }

    interface EventB {
        KindT:'KindTB'
        EventB: 'EventB'
    }

    interface EventC {
        KindT:'KindTC'
        EventC: 'EventC'
    }

    type EventArgs = [number, string]

    type Items = {
        KindTA : EventA,
        KindTB : EventB,
        KindTC : EventC
        //0 : EventArgs,
    }

    type DiscriminatorKindTypeUnionCache = RecordToDiscriminateKindCache<string 
    //| number,
    "KindGen", Items>;

    type CachedItemForSpeed = DiscriminatorKindTypeUnionCache['KindTB']

    type DiscriminatorKindTypeUnion = DiscriminateKindFromCache<DiscriminatorKindTypeUnionCache>;

    function example() {
        
        const test: DiscriminatorKindTypeUnion;
        switch(test.KindGen) {
            case 'KindTA':
                test.EventA
                break;
            case 'KindTB':
                test.EventB
                break;
            case 'KindTC':
                test.EventC

            case 0:
                test.toLocaleString

        }
    }


    type EmitterConfig = EventEmitterConfig<string 
    //| number
    , any, Items>;

    const EmitterInstance :TDEventEmitter<EmitterConfig>;

    EmitterInstance.on("KindTB",(a, b) => {
        
        a.

    })

根据我最近看到或参与的所有讨论,类型和接口之间的主要区别在于接口可以扩展而类型不能。

此外,如果您声明一个接口两次,它们将合并为一个接口。你不能用类型来做。

From the official docs

Differences Between Type Aliases and Interfaces Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

界面是专门为描述物体形状而设计的;然而 Types 在某种程度上类似于可用于为任何类型创建新名称的接口。

我们可能会说 Interface 可以通过多次声明来扩展;而类型是封闭的。

https://itnext.io/interfaces-vs-types-in-typescript-cf5758211910

2022 年更新 -

Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces