TS 接口
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 1
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
什么是接口(Interface)?
TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法(会走路、游泳和呱呱叫的鸟就是鸭子)”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
接口初探(Our First Interface)
interface People {
name: string,
age: number
}
function printName(people: People): void {
console.log(people.name);
}
const p_1 = { name: "Jack", age: 26 };
const p_2 = { age: 28, name: "Evan" };
printName(p_1);
printName(p_2);
People接口就好比一个名字,用来描述上述例子接收参数的结构。和其它语言接口不同的是,接口并不是传给printName的对象实现的,我们只关注值的外形,只要传入的对象满足上述提到的必要条件,那么它就是匹配的。另外,类型检查器并不会去检测属性的顺序,只要属性存在并且类型对应即可。
编译后对应的JavaScript如下:
function printName(people) {
console.log(people.name);
}
var p_1 = { name: "Jack", age: 26 };
var p_2 = { age: 28, name: "Evan" };
printName(p_1);
printName(p_2);
可以看到,编译后接口部分并没有对应的代码。
可选属性(Optional Properties)
接口里的属性并不总是必须的。我们可以通过在接口属性后面加一个 ”?“ 符号将其标记为可选属性。
interface Square {
color: string,
area: number
}
interface SquareConfig {
color?: string,
width?: number
}
function createSquare(config: SquareConfig): Square {
let newSquare = { color: "white", area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
createSquare({color: "black"});
如果我们不小心拼错属性名,检测器会给出如下提示:

额外的属性检查(Excess Property Checks)
上面的例子中,假如我们传入非接口SquareConfig中定义的属性,会怎么样?
createSquare({ color: "black", opacity: 0.5 });
检测器会给出如下提示:

那么,我们如何绕过额外属性的检查呢?
1、使用as类型断言:
createSquare({ color: "black", opacity: 0.5 } as SquareConfig);
2、接口中添加一个字符串类型的索引签名,并将它的类型设置为any:
interface SquareConfig {
color?: string,
width?: number,
[propName: string]: any
}
TIPS: 这里索引名称中的
propName除了增加可读性外,并没有什么实际意义。
然而绕过这些检查可能会遗漏真正的错误。我们应该重新审查接口的定义。上面例子中,可以通过修改SquareConfig支持传入opacity属性来解决问题。
interface SquareConfig {
color?: string,
width?: number,
opacity?: number
}
只读属性(Readonly properties)
一些对象的属性在创建后不允许修改,我们可以通过在属性名前用readonly来指定。
interface Point {
readonly x: number;
readonly y: number;
}
let point_1: Point = { x: 10, y: 20 };
point_1.x = 20; // ❌
TypeScript 提供了ReadonlyArray<T> 类型,我们可以通过它创建一个不允许修改的数组。
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // ❌
ro.push(5); // ❌
ro.length = 100; // ❌
a = ro; // ❌
某些情况下,我们可以通过类型断言跳过这个限制(相信我,我知道自己在干什么)。
(ro as number[])[0] = 12; // ✔️
(ro as number[]).push(5); // ✔️
(ro as number[]).length = 100; // ✔️
a = ro as number[]; // ✔️
函数类型(Function Types)
接口除了描述带有属性的普通对象外,也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
interface SearchFunc {
(source: string, subString: string): boolean;
}
const mySearch: SearchFunc = function (source, subString) {
const result = source.search(subString);
return result > -1;
};
这里函数的参数名不需要和接口中定义的名称完全一致,检查器会逐个对参数进行检查,只需对应位置上的类型匹配就可以。
const mySearch: SearchFunc = function (src, sub) {
const result = src.search(sub);
return result > -1;
};
可索引的类型(Indexable Types)
与描述函数类型差不多,接口也可以描述那些能够“通过索引得到”的类型,比如a[10] or ageMap["daniel"]。可索引类型具有一个索引签名,用来描述对象索引的类型以及索引相对应的返回值类型。例如:
interface StringArray {
[index: number]: string
}
let myArray: StringArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
上述示例中,我们定义了StringArray接口,它具有一个索引签名。这个索引签名描述了当用number去索引StringArray时会得到一个string类型的返回值。
当你声明一个索引签名时,所有明确的成员都必须符合索引签名:
interface Foo {
[key: string]: number,
x: number,
y: number
}
interface Bar {
[key: string]: number,
x: number,
y: string // ❌
}
TypeScript支持string和number两种索引签名。也可以同时支持两种签名,但是数字索引的返回值必须是字符串索引返回值的子类型。这是因为当使用number来索引时,JavaScript会将它转换成string(调用toString方法)然后再去索引对象。
class Animal {
name: string;
}
class Dog extends Animal {
bark() {
console.log("Woof! Woof!");
}
}
interface NotOkay {
[x: number]: Animal, // ❌ 数字索引类型“Animal”不能赋给字符串索引类型“Dog”。
[y: string]: Dog
}
interface Okay {
[x: number]: Dog,
[y: string]: Animal
}
同其它属性一样,索引签名也支持设置为只读。
interface ReadonlyStringArray {
readonly [index: number]: string
}
const arr: ReadonlyStringArray = ["Tom", "Bob"];
arr[1] = "Ali"; // ❌
类类型(Class Types)
同C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。
interface ClockInterface {
currentTime: Date
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
我们也可以在接口中描述一个方法,在类中实现它。
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { };
setTime(d: Date) {
this.currentTime = d;
}
}
注意接口只描述了类的公共部分,并不包含私有部分。
当你用构造器签名去定义一个接口并试图通过类去实现这个接口时会得到一个错误:
interface ClockInterface {
currentTime: Date;
new (hour: number, mimute: number);
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }; // ❌ 类型“Clock”提供的内容与签名“new (hour: number, mimute: number): any”不匹配。
}
这是因为当一个类实现一个接口时,只对其实例部分进行了类型检查。constructor存在于类的静态部分,不在其检查范围内。
下面的例子中,我们定义了两个接口,ClockConstructor和ClockInterface。为了方便我们定义了一个构造函数createClock,它通过传入的类型创建实例。
interface ClockConstructor {
new (hour: number, mimute: number)
}
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");
}
}
const digital = createClock(DigitalClock, 17, 20);
const analog = createClock(AnalogClock, 17, 21);
因为createClock的第一个参数是ClockConstructor类型,在createClock(DigitalClock, 17, 20)中会检查DigitalClock和AnalogClock是否符合构造函数签名。
另外一种的简单的方式是使用类表达式
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) { };
tick() {
console.log("beep beep");
}
}
继承接口(Extending Interfaces)
和类一样,接口也可以相互继承。这让我们能够从一个接口复制成员到另外一个接口,可以更灵活的将接口分割到可重用的的模块里。
interface Shape {
color: string
}
interface Square extends Shape {
sideLength: number
}
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
一个接口也可以同时继承多个接口。
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;
混合类型(Hybrid Types)
因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。
常见的一个例子就是,一个对象可以同时做为函数和对象使用。
interface Counter {
(start: number): string;
interval: number;
reset(): void
}
function getCounter(): Counter {
let counter = (function (start) {}) as Counter;
counter.interval = 123;
counter.reset = function () {};
return counter;
}
let c = getCounter();
c(10);
c.interval = 456;
c.reset();
在使用JavaScript第三方库时,你可能需要像上面那样去完整的定义类型。
接口继承类(Interfaces Extending Classes)
当接口继承一个类类型时,它会继承类的成员但不包括其实现。就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。接口同样会继承到类的private和protected 成员。这意味着当你创建了一个接口继承了拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现。
当你有一个庞大的继承结构时这很有用,但要指出的是你的代码只在子类拥有特定属性时起作用。这个子类除了继承至基类外与基类没有任何关系。例如:
class Control {
private state: boolean
}
interface SelectableControl extends Control {
select(): void
}
class Button extends Control implements SelectableControl {
select() {}
}
class TextBox extends Control {
select() {}
}
// ❌ Property 'state' is missing in type 'ImageC' but required in type 'SelectableControl'.ts(2420)
class ImageC implements SelectableControl {
select() {}
}
上面例子中,SelectableControl包含了Control的所有成员,包括私有成员state。因为state是私有成员,所以只能够是Control的子类才能实现SelectableControl接口。因为只有Control的子类才能够声明Control的私有成员state,这对私有成员的兼容性是必须的。
在Control类内部,是允许通过SelectableControl的实例来访问私有成员state的。实际上,SelectableControl接口和拥有select方法的Control的类是一样的。Button和TextBox类是SelectableControl的子类(因为它们都继承至Control并有select方法),但ImageC类并不是这样的。
Refs:
» Interfaces
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
The issue contains a Chinese TypeScript interfaces article and links to the TypeScript Interfaces handbook page, but it names no repository file, test, or specific change. First inspect the repository structure and existing documentation conventions; a clear done condition is not provided.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- documentation
- Issue type
- Documentation
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100