此页面已过时

本手册页面已被替换,请前往新页面

接口 (Interfaces)

TypeScript 的核心原则之一是类型检查侧重于值所具有的“形状”。这有时被称为“鸭子类型”或“结构化子类型”。在 TypeScript 中,接口(Interface)担任了为这些类型命名的角色,是定义代码内部契约以及与项目外部代码契约的强大方式。

我们的第一个接口

了解接口如何工作的最简单方法是从一个简单的例子开始

ts
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}
 
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Try

类型检查器会检查对 printLabel 的调用。printLabel 函数有一个单一参数,要求传入的对象必须包含一个名为 label 且类型为 string 的属性。请注意,我们的对象实际上拥有比这更多的属性,但编译器只会检查至少是否存在所需的属性,且其类型是否匹配。在某些情况下,TypeScript 的要求会更严格,我们稍后会介绍。

我们可以再次编写相同的示例,这次使用接口来描述“必须拥有一个类型为字符串的 label 属性”这一要求

ts
interface LabeledValue {
label: string;
}
 
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
 
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Try

接口 LabeledValue 是一个我们现在可以用来描述上一个示例中要求的名称。它仍然表示包含一个名为 label 且类型为 string 的属性。请注意,我们不需要像在其他语言中那样显式声明传入 printLabel 的对象“实现了”这个接口。在这里,只有形状重要。如果我们传递给函数的对象满足列出的要求,那么它就是被允许的。

值得指出的是,类型检查器并不要求这些属性按任何特定的顺序排列,它只要求接口所需的属性必须存在且具有所需的类型。

可选属性

并非接口的所有属性都是必须的。有些属性在特定条件下存在,或者可能根本不存在。这些可选属性在创建“配置选项(option bags)”之类的模式时非常流行,例如当你向函数传递一个只填充了几个属性的对象时。

以下是这种模式的一个示例

ts
interface SquareConfig {
color?: string;
width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
 
let mySquare = createSquare({ color: "black" });
Try

带有可选属性的接口与其他接口的编写方式类似,每个可选属性在声明时名称末尾都会加上一个 ?

可选属性的优势在于,你可以描述这些可能存在的属性,同时还能阻止使用接口中未定义的属性。例如,如果我们拼错了 createSquarecolor 属性的名称,我们会得到一条错误消息提示

ts
interface SquareConfig {
color?: string;
width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: "white", area: 100 };
if (config.clor) {
Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?2551Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?2551Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'?
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
 
let mySquare = createSquare({ color: "black" });
Try

只读属性 (Readonly properties)

有些属性应该只在对象首次创建时被修改。你可以通过在属性名称前添加 readonly 来指定这一点

ts
interface Point {
readonly x: number;
readonly y: number;
}
Try

你可以通过分配对象字面量来构造一个 Point。赋值后,xy 就不能再被更改。

ts
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
Cannot assign to 'x' because it is a read-only property.2540Cannot assign to 'x' because it is a read-only property.
Try

TypeScript 自带一个 ReadonlyArray<T> 类型,它与 Array<T> 相同,但删除了所有会改变数组的方法,因此你可以确保在创建数组后不会对其进行修改

ts
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
 
ro[0] = 12; // error!
Index signature in type 'readonly number[]' only permits reading.2542Index signature in type 'readonly number[]' only permits reading.
ro.push(5); // error!
Property 'push' does not exist on type 'readonly number[]'.2339Property 'push' does not exist on type 'readonly number[]'.
ro.length = 100; // error!
Cannot assign to 'length' because it is a read-only property.2540Cannot assign to 'length' because it is a read-only property.
a = ro; // error!
The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.4104The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
Try

在代码片段的最后一行,你可以看到即使将整个 ReadonlyArray 重新赋值给一个普通数组也是不合法的。不过,你仍然可以通过类型断言来覆盖它

ts
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
 
a = ro as number[];
Try

readonlyconst

记住是使用 readonly 还是 const 的最简单方法是:询问自己是在变量还是属性上使用它。变量使用 const,而属性使用 readonly

额外属性检查 (Excess Property Checks)

在我们使用接口的第一个例子中,TypeScript 允许我们将 { size: number; label: string; } 传递给只期望 { label: string; } 的函数。我们刚刚也学习了可选属性,以及它们在描述所谓的“配置选项”时的作用。

然而,天真地结合这两者可能会导致错误潜入。例如,使用上一个 createSquare 的例子

ts
interface SquareConfig {
color?: string;
width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || "red",
area: config.width ? config.width * config.width : 20,
};
}
 
let mySquare = createSquare({ colour: "red", width: 100 });
Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2561Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
Try

注意传递给 createSquare 的参数拼写成了 colour 而不是 color。在纯 JavaScript 中,这类事情会悄无声息地失败。

你可能会争辩说这个程序是正确键入的,因为 width 属性是兼容的,color 属性不存在,而多余的 colour 属性并不重要。

然而,TypeScript 的立场是认为这段代码可能存在错误。对象字面量在赋值给其他变量或作为参数传递时会受到特殊处理,并进行额外属性检查。如果对象字面量包含任何“目标类型”中没有的属性,你将会得到一个错误

ts
let mySquare = createSquare({ colour: "red", width: 100 });
Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?2561Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
Try

绕过这些检查其实非常简单。最简单的方法是使用类型断言

ts
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
Try

不过,如果你确定对象可以包含一些以特殊方式使用的额外属性,更好的方法可能是添加一个字符串索引签名。如果 SquareConfig 可以有上述类型的 colorwidth 属性,但可以有任意数量的其他属性,那么我们可以这样定义它

ts
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
Try

我们稍后会讨论索引签名,但这里我们表达的是 SquareConfig 可以有任意数量的属性,只要它们不是 colorwidth,它们的类型无关紧要。

绕过这些检查的最后一种方法(可能有点出人意料)是将该对象赋值给另一个变量:由于 squareOptions 不会进行额外属性检查,编译器不会报错。

ts
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
Try

只要 squareOptionsSquareConfig 之间有共同属性,上述变通方法就有效。在这个例子中,共同属性是 width。然而,如果变量没有任何共同的对象属性,它就会失败。例如

ts
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);
Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.2559Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.
Try

请记住,对于像上面这样的简单代码,你可能不应该试图“绕过”这些检查。对于具有方法和状态的更复杂的对象字面量,你可能需要记住这些技术,但大多数额外属性错误实际上确实是 Bug。这意味着如果你在处理类似“配置选项”时遇到额外属性检查问题,你可能需要修改你的类型声明。在这种情况下,如果允许将带有 colorcolour 属性的对象传递给 createSquare,你应该修正 SquareConfig 的定义来反映这一点。

函数类型 (Function Types)

接口能够描述 JavaScript 对象可以呈现的各种形状。除了描述带有属性的对象外,接口还能够描述函数类型。

要使用接口描述函数类型,我们要为接口提供一个调用签名。这就像一个仅指定了参数列表和返回类型的函数声明。参数列表中的每个参数都需要名称和类型。

ts
interface SearchFunc {
(source: string, subString: string): boolean;
}
Try

定义好之后,我们可以像使用其他接口一样使用这个函数类型接口。在这里,我们展示了如何创建一个函数类型的变量,并为其赋值一个相同类型的函数值。

ts
let mySearch: SearchFunc;
 
mySearch = function (source: string, subString: string): boolean {
let result = source.search(subString);
return result > -1;
};
Try

为了使函数类型正确通过类型检查,参数的名称不需要匹配。例如,我们可以这样编写上面的例子

ts
let mySearch: SearchFunc;
 
mySearch = function (src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
};
Try

函数参数是逐个检查的,每个对应位置的参数类型都会进行相互比对。如果你完全不想指定类型,TypeScript 的上下文类型推断可以自动推断参数类型,因为函数值是直接赋值给 SearchFunc 类型的变量。在这里,我们的函数表达式的返回类型也是根据它返回的值(这里是 falsetrue)隐式推断的。

ts
let mySearch: SearchFunc;
 
mySearch = function (src, sub) {
let result = src.search(sub);
return result > -1;
};
Try

如果函数表达式返回了数字或字符串,类型检查器会报错,指出返回类型与 SearchFunc 接口中描述的返回类型不匹配。

ts
let mySearch: SearchFunc;
 
mySearch = function (src, sub) {
Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. Type 'string' is not assignable to type 'boolean'.2322Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'. Type 'string' is not assignable to type 'boolean'.
let result = src.search(sub);
return "string";
};
Try

索引类型 (Indexable Types)

类似于我们可以使用接口来描述函数类型,我们也可以描述我们可以“索引”的类型,比如 a[10]ageMap["daniel"]。索引类型具有一个索引签名,它描述了我们可以用来索引对象的类型,以及索引时的相应返回类型。

让我们举个例子

ts
interface StringArray {
[index: number]: string;
}
 
let myArray: StringArray;
myArray = ["Bob", "Fred"];
 
let myStr: string = myArray[0];
Try

上面我们有一个带有索引签名的 StringArray 接口。该索引签名声明当使用 number 索引 StringArray 时,它将返回一个 string

支持四种类型的索引签名:string、number、symbol 和模板字符串。支持多种类型的索引器是可能的,但从数字索引器返回的类型必须是字符串索引器返回类型的子类型。

这是因为当使用 number 进行索引时,JavaScript 实际上会在索引对象之前将其转换为 string。这意味着使用 100(数字)索引与使用 "100"(字符串)索引是一样的,因此两者需要保持一致。

ts
interface Animal {
name: string;
}
 
interface Dog extends Animal {
breed: string;
}
 
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.2413'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
[x: string]: Dog;
}
Try

虽然字符串索引签名是描述“字典”模式的强大方式,但它们也强制要求所有属性必须符合其返回类型。这是因为字符串索引声明了 obj.property 也可以作为 obj["property"] 使用。在下面的例子中,name 的类型与字符串索引的类型不匹配,类型检查器会报错

ts
interface NumberDictionary {
[index: string]: number;
 
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
Property 'name' of type 'string' is not assignable to 'string' index type 'number'.2411Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}
Try

然而,如果索引签名是属性类型的联合类型,那么不同类型的属性是可以接受的

ts
interface NumberOrStringDictionary {
[index: string]: number | string;
 
length: number; // ok, length is a number
name: string; // ok, name is a string
}
Try

最后,你可以将索引签名设为 readonly,以防止对其索引进行赋值

ts
interface ReadonlyStringArray {
readonly [index: number]: string;
}
 
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
Index signature in type 'ReadonlyStringArray' only permits reading.2542Index signature in type 'ReadonlyStringArray' only permits reading.
Try

你不能设置 myArray[2],因为索引签名是 readonly

带模板字符串的索引类型

模板字符串可用于指示允许特定的模式,但并非全部允许。例如,HTTP 标头对象可能有一组已知的标头列表,并支持任何以 x- 为前缀的自定义属性

ts
interface HeadersResponse {
"content-type": string,
date: string,
"content-length": string
 
// Permit any property starting with 'x-'.
[headerName: `x-${string}`]: string;
}
 
function handleResponse(r: HeadersResponse) {
// Handle known, and x- prefixed
const type = r["content-type"]
const poweredBy = r["x-powered-by"]
 
// Unknown keys without the prefix raise errors
const origin = r.origin
Property 'origin' does not exist on type 'HeadersResponse'.2339Property 'origin' does not exist on type 'HeadersResponse'.
}
Try

类类型 (Class Types)

实现接口 (Implementing an interface)

在 C# 和 Java 等语言中,接口最常见的用途之一是显式强制类满足特定的契约,这在 TypeScript 中也是可能的。

ts
interface ClockInterface {
currentTime: Date;
}
 
class Clock implements ClockInterface {
currentTime: Date = new Date();
constructor(h: number, m: number) {}
}
Try

你也可以在接口中描述在类中实现的方法,就像我们在下面的例子中对 setTime 所做的那样

ts
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
 
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {}
}
Try

接口描述的是类的公共部分,而不是公共和私有部分。这禁止你使用它们来检查类是否也为类的实例的私有部分包含了特定类型。

类静态部分与实例部分的区别

在使用类和接口时,记住类有两种类型是有帮助的:静态部分的类型和实例部分的类型。你可能会注意到,如果你创建一个带有构造签名的接口,并尝试创建一个实现该接口的类,你会得到一个错误

ts
interface ClockConstructor {
new (hour: number, minute: number);
}
 
class Clock implements ClockConstructor {
Class 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.2420Class 'Clock' incorrectly implements interface 'ClockConstructor'. Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.
currentTime: Date;
constructor(h: number, m: number) {}
}
Try

这是因为当类实现接口时,只检查类的实例部分。由于构造函数位于静态部分,它不包含在检查中。

相反,你需要直接处理类的静态部分。在这个例子中,我们定义了两个接口,ClockConstructor 用于构造函数,ClockInterface 用于实例方法。然后,为了方便起见,我们定义了一个构造函数 createClock,它创建传递给它的类型的实例

ts
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
 
interface ClockInterface {
tick(): void;
}
 
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}
 
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
}
 
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("tick tock");
}
}
 
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
Try

因为 createClock 的第一个参数是 ClockConstructor 类型,在 createClock(AnalogClock, 7, 32) 中,它会检查 AnalogClock 是否具有正确的构造函数签名。

另一种简单的方法是使用类表达式

ts
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
 
interface ClockInterface {
tick(): void;
}
 
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
};
 
let clock = new Clock(12, 17);
clock.tick();
Try

继承接口 (Extending Interfaces)

像类一样,接口可以相互继承。这允许你将一个接口的成员复制到另一个接口中,从而在如何将接口分离为可重用组件方面为你提供了更大的灵活性。

ts
interface Shape {
color: string;
}
 
interface Square extends Shape {
sideLength: number;
}
 
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
Try

一个接口可以继承多个接口,从而创建所有接口的组合。

ts
interface Shape {
color: string;
}
 
interface PenStroke {
penWidth: number;
}
 
interface Square extends Shape, PenStroke {
sideLength: number;
}
 
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
Try

混合类型 (Hybrid Types)

正如我们之前提到的,接口可以描述现实世界 JavaScript 中存在的丰富类型。由于 JavaScript 的动态和灵活性,你偶尔会遇到一个既是上述某些类型组合的对象。

一个例子是既充当函数又充当对象,且具有额外属性的对象

ts
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
 
function getCounter(): Counter {
let counter = function (start: number) {} as Counter;
counter.interval = 123;
counter.reset = function () {};
return counter;
}
 
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
Try

在与第三方 JavaScript 交互时,你可能需要使用上述模式来完整描述该类型的形状。

接口继承类 (Interfaces Extending Classes)

当接口类型继承类类型时,它会继承类的成员,但不继承其实现。这就好比接口声明了类的所有成员,但没有提供实现。接口甚至继承了基类的私有和受保护成员。这意味着当你创建一个继承了具有私有或受保护成员的类的接口时,该接口类型只能由该类或其子类来实现。

当你拥有庞大的继承层次结构,但只想指定你的代码仅适用于具有特定属性的子类时,这非常有用。除了从基类继承之外,这些子类不需要有其他关系。例如

ts
class Control {
private state: any;
}
 
interface SelectableControl extends Control {
select(): void;
}
 
class Button extends Control implements SelectableControl {
select() {}
}
 
class TextBox extends Control {
select() {}
}
 
class ImageControl implements SelectableControl {
Class 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.2420Class 'ImageControl' incorrectly implements interface 'SelectableControl'. Types have separate declarations of a private property 'state'.
private state: any;
select() {}
}
Try

在上面的例子中,SelectableControl 包含了 Control 的所有成员,包括私有的 state 属性。因为 state 是私有成员,所以只有 Control 的后代才能实现 SelectableControl。这是因为只有 Control 的后代才会有源自同一声明的 state 私有成员,这是私有成员兼容性的要求。

Control 类中,可以通过 SelectableControl 的实例访问私有的 state 成员。实际上,SelectableControl 的作用就像是一个已知具有 select 方法的 ControlButtonTextBox 类是 SelectableControl 的子类型(因为它们都继承自 Control 并拥有 select 方法)。ImageControl 类有自己的私有 state 成员,而不是继承自 Control,所以它不能实现 SelectableControl

TypeScript 文档是一个开源项目。欢迎通过发送 Pull Request 帮助我们改进这些页面 ❤

此页面的贡献者
RCRyan Cavanaugh (55)
DRDaniel Rosenwasser (25)
OTOrta Therox (23)
Jjswheeler (3)
MHMohamed Hegazy (3)
44+

最后更新:2026 年 3 月 27 日