TypeScript中的这些语句(接口与类型)有什么区别?
interface X {
a: number
b: string
}
type X = {
a: number
b: string
};
TypeScript中的这些语句(接口与类型)有什么区别?
interface X {
a: number
b: string
}
type X = {
a: number
b: string
};
2021 3月更新:更新的TypeScript手册(也在nju-clc中提到下面的答案)有一节“接口与类型别名”,解释了区别。
原始答案(2016)
根据(现已存档)TypeScript语言规范:
与总是引入命名对象类型的接口声明不同,类型别名声明可以引入任何类型的名称,包括基元类型、联合类型和交集类型。
该规范还提到:
接口类型与对象类型的类型别名有许多相似之处但是由于接口类型提供了更多的功能通常优选键入别名。例如,接口类型接口点{x: 数量;y: 数量;}可以写成类型别名类型点={x: 数量;y: 数量;};但是,这样做意味着失去以下功能:接口可以在extends或implements子句中命名,但对象类型文本的类型别名不能在TS 2.7之后为true。接口可以有多个合并声明,但对象类型文本的类型别名不能。
https://www.typescriptlang.org/docs/handbook/advanced-types.html
一个区别是,接口创建了一个新名称,该名称在任何地方都可以使用。键入别名不会创建新名称-例如,错误消息不会使用别名。
2019年更新
目前的答案和官方文件已经过时。对于那些新接触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.工具
类可以以相同的方式实现接口或类型别名。但是请注意,类和接口被视为静态蓝图。因此,它们不能实现/扩展命名联合类型的类型别名。
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 };
类型示例:
//为对象创建树结构。由于缺少交集(&),您无法对接口执行相同的操作
type Tree<T> = T & { parent: Tree<T> };
//键入以限制变量仅分配几个值。接口没有联合(|)
type Choise = "A" | "B" | "C";
//由于类型,您可以通过条件机制声明NonNullable类型。
type NonNullable<T> = T extends null | undefined ? never : T;
界面示例:
//您可以使用OOP接口,并使用“implements”定义对象/类骨架
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
}
文件已经解释了
一个区别是,接口创建了一个新名称,该名称在任何地方都可以使用。类型别名不会创建新名称-例如,错误消息不会使用别名名称。在旧版本的TypeScript中,类型别名无法从扩展或实现(也无法扩展/实现其他类型)。从2.7版起,可以通过创建新的交叉点类型来扩展类型别名另一方面,如果不能用接口表达某种形状,并且需要使用联合或元组类型,则通常使用类型别名。
接口与类型别名
其他答案很棒!Type可以做但Interface不能做的其他事情很少
可以在类型中使用联合
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",
};
类型中的交集(但是,在带有扩展的接口中也支持)
type Name = {
firstName: string;
lastName: string;
};
type Address = {
city: string;
};
const person: Name & Address = {
firstName: "jon",
lastName: "doe",
city: "scranton",
};
与接口相比,这种类型也不是后来才引入的,根据最新发布的TS类型,它几乎可以做任何接口可以做的事情,而且更多!
*除了声明合并(个人观点:很好,它在类型中不受支持,因为它可能导致代码不一致)
接口与类型
接口和类型用于描述对象和原语的类型。接口和类型通常可以互换使用,并且通常提供类似的功能。通常由程序员选择自己的偏好。
然而,接口只能描述创建这些对象的对象和类。因此,必须使用类型来描述字符串和数字等原语。
下面是接口和类型之间的两个区别的示例:
// 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 Nullable<T> = T | null | undefined
type NonNull<T> = T extends (null | undefined) ? never : T
类型别名
我们可以使用该类型为长类型或复杂类型创建别名,这些类型既难以阅读,又不方便反复键入。
例子:
type Primitive = number | string | boolean | null | undefined
创建这样的别名使代码更加简洁易读。
类型捕获
当类型未知时,使用该类型捕获对象的类型。
例子:
const orange = { color: "Orange", vitamin: "C"}
type Fruit = typeof orange
let apple: Fruit
在这里,我们得到了未知类型的橙子,称其为水果,然后使用水果创建一个新型的安全对象苹果。
何时使用界面?
多态性
接口是实现数据形状的契约。使用该接口明确表示它将被实现并用作关于如何使用对象的约定。
例子:
interface Bird {
size: number
fly(): void
sleep(): void
}
class Hummingbird implements Bird { ... }
class Bellbird implements Bird { ... }
尽管您可以使用类型来实现这一点,但Typescript更多地被视为一种面向对象的语言,并且接口在面向对象语言中具有特殊的地位。当您在团队环境中工作或为开源社区做出贡献时,使用界面更容易阅读代码。对于来自其他面向对象语言的新程序员来说也很容易。
官方的Typescript文档还说:
…我们建议尽可能在类型别名上使用接口。
这也表明该类型更适合于创建类型别名,而不是创建类型本身。
声明合并
您可以使用接口的声明合并功能向已声明的接口添加新的财产和方法。这对于第三方库的环境类型声明很有用。当第三方库缺少某些声明时,可以使用相同的名称再次声明接口,并添加新的财产和方法。
例子:
我们可以扩展上述Bird接口以包含新的声明。
interface Bird {
color: string
eat(): void
}
就是这样!记住什么时候使用比迷失在两者之间的细微差别中更容易。
还有一个不同之处。我会的。。。如果你能解释这种情况的原因,请给你买一杯啤酒:
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
相关问题:类型中缺少索引签名(仅在接口上,而不是在类型别名上)
因此,如果您想为对象编制索引,请考虑这个示例
看看这个问题和这个关于违反利斯科夫原则的问题
评估中的差异
当FirstLevelType为接口时,请查看ExtendeFirst的结果类型
/**
* 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为类型时,请查看ExtendeFirst的结果类型:
/**
* 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";
就编译速度而言,组合接口的性能优于类型交集:
[…]接口创建检测属性冲突的单个平面对象类型。这与交叉点类型不同,在交叉点类型中,在对照有效类型进行检查之前,检查每个组成部分。接口之间的类型关系也被缓存,而不是交叉类型。
资料来源:https://github.com/microsoft/TypeScript/wiki/Performance#preferring-交叉口上的接口
文档中指出的关键区别在于,可以重新打开接口以添加新属性,但不能重新打开类型别名以添加新的属性,例如:
这没问题
interface x {
name: string
}
interface x {
age: number
}
这将抛出错误Duplicate identifier y
type y = {
name: string
}
type y = {
age: number
}
除此之外,接口和类型别名类似。
在typescript中,建议使用“interface”而不是“type”。
“type”用于创建类型别名。您不能使用“接口”执行此操作。type Data=字符串然后,您可以使用“数据”代替字符串const name:string=“Yilmaz”const name:Data=“Yilmaz”
别名非常有用,尤其是在处理泛型类型时。
声明合并:可以合并接口,但不能合并类型。界面人员{name:字符串;}界面人员{年龄:数字;}//我们必须提供两个人的财产常量名称:人={名称:“Yilmaz”,年龄:30岁};函数式编程用户使用“类型”,面向对象编程用户选择“接口”您不能在接口上计算或计算财产,而是在类型中。type Fullname=“name”|“lastname”类型人员={[key in Keys]:字符串}常量名称:人={名字:“Yilmaz”,姓氏:“Bingol”}
演示递归地重写Object-Literal类型和接口而不是类成员/财产/函数的能力。
此外,当Record<any,string|number>由于接口等原因而无法工作时,如何区分和键入检查差异以及解决上述问题的方法也可以解决。这将允许对猫鼬类型进行以下简化:https://github.com/wesleyolis/mongooseRelationalTypesmongooseRelationalTypes、DeepPopulate、populate
此外,还有一系列其他方法来实现高级类型泛型和类型推理,以及围绕它的速度怪癖,这些都是一些小技巧,可以通过多次试验和错误来获得正确的结果。
打字游戏场:单击此处查看现场游戏场地中的所有示例
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.
})
根据我最近看到或参与的所有讨论,类型和接口之间的主要区别在于接口可以扩展,而类型不能扩展。
此外,如果您两次声明一个接口,它们将被合并为一个接口。你不能用打字。
来自官方文件
类型别名和接口之间的差异类型别名和接口非常相似,在许多情况下,您可以在它们之间自由选择。接口的几乎所有功能都可以在类型中使用,关键区别在于,与总是可扩展的接口相比,不能重新打开类型来添加新的财产。
界面专门设计用于描述对象形状;然而,类型在某种程度上类似于可以用于为任何类型创建新名称的接口。
我们可以说,一个接口可以通过多次声明来扩展;而类型是封闭的。
https://itnext.io/interfaces-vs-types-in-typescript-cf5758211910
2022年更新-
类型别名和接口非常相似,在许多情况下可以自由选择。界面的几乎所有功能在类型中可用,关键区别是类型不能重新打开以添加新的财产,而不是始终可延伸。
https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-在类型别名和接口之间
想加上我的2美分;
我曾经是“界面爱好者”(除了联合、交叉等以外,我更喜欢界面而不是类型)。。。直到我开始使用类型“any key value object”,即Record<string,unknown>
如果键入“任意键值对象”:
function foo(data: Record<string, unknown>): void {
for (const [key, value] of Object.entries(data)) {
// whatever
}
}
如果你使用接口,你可能会走到死胡同
interface IGoo {
iam: string;
}
function getGoo(): IGoo {
return { iam: 'an interface' };
}
const goo = getGoo();
foo(goo); // ERROR
// Argument of type 'IGoo' is not assignable to parameter of type
// 'Record<string, unknown>'.
// Index signature for type 'string' is missing in type
// 'IGoo'.ts(2345)
虽然打字就像一个符咒:
type Hoo = {
iam: string;
};
function getHoo(): Hoo {
return { iam: 'a type' };
}
const hoo = getHoo();
foo(hoo); // works
这个特定的用例——IMO——产生了不同。
在我的日常发展中,当我不知道该选择哪一个时,我会使用这个作弊表。
有关更多信息,请阅读我的博客:https://medium.com/@magenta2127/use-which-interface-or-type-alias-in-typescript-bdfaf2e882ae