接口
接口
接口(Interfaces)在 TypeScript 中是定义对象形状的一种方式,可以指定一个对象必须有哪些属性以及属性的类型。接口非常有用,因为它们帮助你定义不同数据结构应该如何看起来,并且 TypeScript 编译器会检查你的代码,确保数据符合接口定义。
基本用法:
interface Person { name: string; age: number; } const student: Person = { name: "Alice", age: 25, };
这段代码定义了一个
Person
接口,它要求任何Person
类型的对象有name
和age
属性,分别是字符串和数字。可选属性:
interface Person { name: string; age?: number; // 年龄是可选的 } const someone: Person = { name: "Bob", };
age
后面的问号表示这个属性是可选的,不一定需要在每个Person
对象中出现。只读属性:
interface User { readonly id: number; name: string; } const user: User = { id: 1, name: "Charlie" }; // user.id = 5; // 错误:id 是只读的
readonly
关键字意味着一旦属性被赋值后就不能再被修改。函数类型:
interface SearchFunc { (source: string, subString: string): boolean; } let mySearch: SearchFunc; mySearch = function (source: string, subString: string) { return source.search(subString) > -1; };
这个
SearchFunc
接口定义了一个函数类型,该函数接受两个字符串参数并返回一个布尔值。索引签名:
interface StringArray { [index: number]: string; } let myArray: StringArray; myArray = ["Bob", "Fred"]; let myStr: string = myArray[0];
StringArray
接口使用索引签名来描述那些能够通过数字索引得到的类型,本例中即为字符串数组。类类型:
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) {} }
使用
implements
关键字可以让类Clock
遵循ClockInterface
的形状,这意味着Clock
类必须实现接口中定义的所有属性和方法。继承接口:
interface Shape { color: string; } interface Square extends Shape { sideLength: number; } let square = <Square>{}; square.color = "blue"; square.sideLength = 10;
Square
接口通过继承Shape
接口获取了color
属性,同时增加了自己的sideLength
属性。
接口初探
接口是 TypeScript 中描述对象形状(shape)的一种方法。一个接口可以告诉 TypeScript 一个对象应该有什么样的属性,这些属性的类型是什么,以及是否是必须的。
以下是接口初探相关的关键点:
- 定义接口:你可以使用
interface
关键字来定义一个接口。这个接口可以用来描述一个对象可能具有的结构。例如:
interface Person {
name: string;
age: number;
}
- 使用接口:一旦定义了接口,你就可以在变量、函数参数或者函数返回类型的地方引用它。例如:
function greet(person: Person) {
return "Hello, " + person.name;
}
let user = { name: "Jane", age: 30 };
console.log(greet(user)); // Output: Hello, Jane
- 可选属性:接口的属性不一定全部是必须的,你可以通过在属性名后面加上
?
符号来标记那些可选的属性。例如:
interface Person {
name: string;
age?: number; // age now is optional
}
let user = { name: "Tom" };
- 只读属性:接口还可以定义只读属性,这意味着一旦一个对象被创建,其只读属性就不能再被修改了。使用
readonly
关键字来指定只读属性。例如:
interface Person {
readonly name: string;
age: number;
}
let user: Person = { name: "John", age: 25 };
user.name = "Jane"; // Error: Cannot assign to 'name' because it is a read-only property.
- 函数类型:接口不仅可以用来描述普通的对象,还可以用来描述函数类型。这就需要指定一个调用签名。例如:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
return source.search(subString) > -1;
};
- 索引签名:当你想让一个对象能够以多种方式被索引时,例如一个 string 做 key 并且返回任意的结果,你可以使用索引签名。例如:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
通过理解和使用接口,你可以更好地利用 TypeScript 的强类型特性来构建更稳定、易于维护的代码。
可选属性
可选属性 在 TypeScript 的接口(Interfaces)中,你可以定义对象拥有哪些属性及其类型。有时候,并不是对象的所有属性都是必需的,这时就可以使用可选属性来定义。
通过在属性名后加一个问号
?
来标记该属性为可选。这意味着这个属性可以存在于对象中,也可以不存在。当定义了一个带有可选属性的接口之后,在实现此接口的对象中,可以含有也可以不含有这些属性。但是如果包含这些属性,其类型必须与接口定义时的类型相匹配。
可选属性给你的代码添加了灵活性,并且可以用来表示那些只能在某些情况下存在的属性。
同时,可选属性也对对象可能存在的属性进行了预定义,这可以帮助避免拼写错误和提供更好的类型检查。
例子:
interface Person {
name: string;
age?: number; // 可选属性
}
function greet(person: Person) {
console.log("Hello, " + person.name);
}
// 这是合法的,因为age是可选的
const person1: Person = { name: "Alice" };
greet(person1);
// 这也是合法的,并且age符合number类型
const person2: Person = { name: "Bob", age: 25 };
greet(person2);
// 下面的代码会产生类型检查错误,因为age的类型不正确
const person3: Person = { name: "Charlie", age: "thirty-five" }; // Error
在上述例子中,我们定义了一个 Person
接口,其中 age
是一个可选属性。这意味着当创建 Person
类型的对象时,可以省略 age
属性。在函数 greet
中,我们只关心每个人的 name
,不管 age
是否存在。
只读属性
接口中的“只读属性”指的是一旦一个属性被赋值后,就不能再被更改。在 TypeScript 中,你可以在接口里用
readonly
关键字标记这些属性。使用
readonly
可以让你的意图变得更加明确,表明某些属性不应该被修改,这有助于预防编程时的错误。
interface Point {
readonly x: number;
readonly y: number;
}
- 在上面的例子中,定义了一个名为 Point 的接口,它有两个只读属性:x 和 y。当你创建了这个接口的对象后,你将无法修改 x 和 y 的值。
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // 错误, x 是只读的
尝试修改 p1 的 x 属性会导致 TypeScript 编译错误,因为 x 是只读的。
readonly
属性也适用于数组,你可以通过ReadonlyArray<T>
来确保数组创建后不会被修改。
let a: ReadonlyArray<number> = [1, 2, 3];
a[0] = 12; // 错误
a.push(5); // 错误
a.length = 100; // 错误
a = [4, 5, 6]; // 可行, 但要注意这是创建了一个全新的数组
在上面的例子中,尝试修改数组 a 的内容(如修改元素、推送新元素或者改变数组长度)都会导致错误,因为 a 是一个只读数组。但是你可以将 a 整个替换为一个新数组,这是允许的操作。
TypeScript 中的
readonly
和 JavaScript 中的const
是类似的,但是const
是用于变量声明,而readonly
是用于属性。const
声明后变量不能重新赋值,而readonly
则是确保属性不被重新赋值。
const point = { x: 10, y: 20 };
point.x = 5; // 错误, x 不可重新赋值
readonly
对于实现不可变性模式非常有用,特别是当你处理复杂数据结构并且想要避免副作用时。
readonly vs const
在 TypeScript 中,readonly
关键字和const
关键字都用于确保变量不被重新赋值,但它们适用的上下文不同。
readonly
:用于接口或类中属性声明,表示该属性一旦初始化后就不能再被修改。
对象的属性可以在创建时设置,但之后就不能更改了。
在接口中使用
readonly
可以让你定义出只能在对象初始创建时赋值的属性。示例:
interface Point { readonly x: number; readonly y: number; } let p1: Point = { x: 10, y: 20 }; p1.x = 5; // Error, x is readonly.
const
:- 不是用在属性上,而是用在变量声明上。
- 意味着该变量的引用不能被改变,它必须初始化,并且之后不能指向另外一个值。
- 通常用于常量或者函数作用域的不变引用。
- 示例:
const num = 10; num = 20; // Error, num is a constant.
需要注意的是,当涉及到数组或对象时,const
关键词只保证变量名指向的引用不变,但对象或数组内部的内容是可以改变的。相比之下,如果你使用readonly
来修饰一个对象数组的话,数组内的每个元素也将变为不可更改。
总结:
- 使用
readonly
来定义对象属性,这些属性一旦被赋值后不应更改; - 使用
const
定义的是变量的引用不可更改。
额外的属性检查
在 TypeScript 中,接口定义了对象必须遵循的形状(即包含哪些属性以及它们的类型)。然而,在使用接口时,如果你给函数传递了一个多出一些未在接口中定义的属性的对象,TypeScript 会发出警告。这就是额外的属性检查。
为什么有额外的属性检查?
- 防止因为拼写错误或者不正确的属性名而引入潜在的 bug。
- 确保函数参数和其他对象字面量的使用符合预期的结构。
如何工作?
- 当你将一个对象字面量直接传递给需要一个特定接口的地方时,TypeScript 会进行额外属性检查。
- 如果对象字面量拥有该接口未定义的属性,编译器会报错。
实例解释:
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj); // Error: 'size' not found in type 'LabelledValue'
在上述例子中,
printLabel
函数期望得到一个LabelledValue
接口的实例,但传给它的myObj
同时含有未在LabelledValue
中定义的size
属性,所以 TypeScript 报错。如何绕过额外属性检查?
使用类型断言:
printLabel(myObj as LabelledValue); // No error
添加一个字符串索引签名到接口(如果你确实需要接口可以拥有其他属性):
interface LabelledValue { label: string; [propName: string]: any; } printLabel(myObj); // No error now, because interface allows any other property
将对象赋值给另一个变量(因为额外属性检查只对对象字面量直接赋值时生效):
let myObj = { size: 10, label: "Size 10 Object" }; let labelledObject: LabelledValue = myObj; printLabel(labelledObject); // No error
通过这种方式,TypeScript 帮助确保你在代码中使用的对象满足特定的形状,并且可以在编译时捕获潜在的错误。
函数类型
在 TypeScript 中,接口不仅可以用于描述对象的形状(shape),还可以描述函数类型。这意味着你可以定义一个接口来确保函数具有特定的签名(即参数列表和返回值类型)。下面是如何使用接口定义函数类型的步骤:
- 声明一个接口。
- 在该接口内部,使用一对圆括号
()
声明一个调用签名,就像你声明一个没有名称的函数一样。 - 在调用签名中,指定参数列表和每个参数的类型,后面跟着一个箭头
=>
,然后是函数的返回值类型。
以下是使用接口定义函数类型的几个例子:
// 定义一个接口,该接口作为函数类型使用
interface SearchFunc {
// 这里定义了一个调用签名
(source: string, subString: string): boolean;
}
// 使用这个接口来定义一个函数
let mySearch: SearchFunc;
// 函数实现必须与接口中定义的类型匹配
mySearch = function (source: string, subString: string) {
// 检查subString是否存在于source中
let result = source.search(subString);
return result > -1; // 如果找到了,返回true
};
// 使用另一个函数签名
interface Transform {
(input: number, factor: number): string;
}
let transformNumber: Transform;
transformNumber = function (input: number, factor: number): string {
return (input * factor).toFixed(2); // 转换为字符串并保留两位小数
};
在上面的例子中,SearchFunc
是一个接口,它定义了一个函数类型,该函数有两个参数(都是字符串),并返回一个布尔值。然后我们声明了 mySearch
变量,并且指定其类型为 SearchFunc
。之后,我们给 mySearch
赋予了一个符合该接口定义的函数实现。
通过这种方式,你可以确保函数遵循预先定义的结构,提高代码的可靠性和可维护性。
可索引的类型
可索引的类型是 TypeScript 中一种用来描述那些能通过索引得到的类型,如数组或对象。在接口中定义一个可索引的类型可以帮助确保正确地使用索引访问值。
- 可索引类型接口的语法类似于对象类型接口,它使用方括号
[]
定义索引签名,说明了当使用索引去访问对象时应该返回什么类型的值。索引签名包括索引类型和相应的返回值类型。 - 索引类型通常是
string
或number
。string
类型索引签名允许你把对象视作字典或映射结构,并使用字符串作为键;而number
类型索引签名主要用于数组或者类数组结构,其索引是数字。
举几个例子:
interface StringArray {
// 使用数字索引,表示通过数字索引得到的是字符串类型的值
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
// 访问第一个元素,TypeScript 知道这将是一个字符串
let first: string = myArray[0];
interface NumberDictionary {
// 使用字符串索引,表示通过字符串索引得到的是数字类型的值
[index: string]: number;
// 你还可以预设某些属性,只要它们的类型与索引返回的类型相符合
length: number; // ok
// name: string; // error: 属性 'name' 的类型不是索引器的子类型
}
interface ReadonlyStringArray {
// 这里定义了一个字符串数组,但它是只读的,意味着你不能改变其元素
readonly [index: number]: string;
}
let myReadOnlyArray: ReadonlyStringArray = ["Alice", "Bob"];
// myReadOnlyArray[2] = "Mallory"; // error: 索引签名在 'ReadonlyStringArray' 类型中仅允许读取
这样,当你定义了一个符合这个接口的变量时,TypeScript 编译器会检查你是否用正确的索引类型去访问对应的值,并且如果你试图对一个只读数组进行修改也会触发编译错误。
类类型
在 TypeScript 中,接口不仅可以用来描述对象的形状或结构,还可以用来描述类的类型。这意味着你可以使用一个接口去描述一个类应该有哪些方法和属性,以及这些方法的参数和返回类型。当一个类实现了一个接口时,它必须提供接口中定义的所有属性和方法。
类类型接口:
- 接口可以被类实现(implements)。
- 实现接口的类必须符合接口定义的结构。
- 接口可以包括属性、函数、索引签名等。
例子:定义接口与实现接口
interface ClockInterface { currentTime: Date; setTime(d: Date): void; } class Clock implements ClockInterface { currentTime: Date = new Date(); setTime(d: Date) { this.currentTime = d; } }
- 这个例子中,
Clock
类实现了ClockInterface
接口。 - 类
Clock
必须包含接口中的currentTime
属性和setTime
方法。
- 这个例子中,
类静态部分与实例部分:
- TypeScript 区分类的静态部分和实例部分。
- 当你用接口检查类时,只能用它来检查类的实例部分。
- 类的静态部分(如构造函数)并不在检查范围内。
构造器签名:
- 如果你想要一个接口去检查类的构造函数,你需要单独定义一个带有构造签名的接口。
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);
- 在这个例子中,我们定义了两个接口:
ClockConstructor
为构造函数的接口,ClockInterface
为实例方法的接口。 createClock
函数接受一个构造函数,时、分参数,并返回一个实现了ClockInterface
的新对象。DigitalClock
和AnalogClock
两个类都实现了ClockInterface
接口,并且它们的构造函数满足ClockConstructor
接口的签名。
通过类类型接口,你可以确保你的类不仅遵循特定的结构,而且也具有接口定义的行为。这在大型项目中尤其有助于保持一致性和可维护性。
实现接口
接口在 TypeScript 中是定义对象的形状的一种方式,它规定了对象应该有哪些属性和方法。当你使用类来实现接口时,你实际上在告诉 TypeScript,你的类将会具备接口所描述的所有属性和方法。
实现接口意味着你创建的类将准确地遵循接口的结构。如果接口声明了某个属性或方法,那么实现了这个接口的类就必须有对应的属性或实现那个方法。
使用
implements
关键字来应用接口到类上。如果类中没有完全按照接口的规定来实现,TypeScript 编译器会抛出错误。接口可以检查类是否具有特定的结构,但不会关心类是如何实现这些结构的。只要成员符合类型要求,类的其他内部实现细节接口并不关心。
例子:
// 定义一个接口
interface AnimalInterface {
name: string;
makeSound(): void; // 必须有一个无参数的方法makeSound
}
// 实现这个接口的类
class Dog implements AnimalInterface {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound() {
console.log("Woof! Woof!");
}
}
// 使用这个类来创建对象
const myDog = new Dog("Buddy");
myDog.makeSound(); // 输出:Woof! Woof!
在上面的例子中,Dog
类通过implements
关键字来实现AnimalInterface
接口。这意味着每个Dog
对象都必须拥有一个名为name
的字符串属性和一个makeSound
的方法。如果我们忽略这些要求之一,在编译时 TypeScript 就会报错。
如果你已经学习了 JavaScript,需要适应 TypeScript 的话,理解接口可以帮助你更好地管理大型代码库中的类型规范,适用于多人协作的项目。接口强制保持一致性,使得代码更容易理解与维护。
类静态部分与实例部分的区别
在 TypeScript 中,当我们使用类与接口时,了解类的静态部分与实例部分的区别是非常重要的。这个概念可以帮助你更好地理解如何通过接口来强制约束类的结构。
类的实例部分指的是那些被实例化后可访问的属性和方法。简单来说,就是你通过
new
关键字创建一个类的实例后,能够用这个实例调用的方法或访问的属性。类的静态部分则指的是直接挂载在类上的属性和方法,而不是类的实例上。这些属性和方法通过类名直接访问,而不需要创建类的实例。
接下来,举几个实用的例子说明:
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick(): void;
}
// 实现接口的类
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");
}
}
// 这个函数创建一个钟表的实例
function createClock(
ctor: ClockConstructor,
hour: number,
minute: number
): ClockInterface {
return new ctor(hour, minute);
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
在这个例子中:
我们定义了两个接口:
ClockConstructor
(构造器签名)和ClockInterface
(实例方法)。ClockConstructor
代表一个构造器签名,它定义了如何通过传入小时和分钟来创建一个ClockInterface
类型的对象。DigitalClock
和AnalogClock
类都实现了ClockInterface
接口,表示它们都有一个tick
方法。但是,我们没有直接在接口中约束类的构造函数,因为接口在 TypeScript 中不能直接描述类的构造函数。createClock
函数演示了如何使用构造器签名。它接受一个符合ClockConstructor
接口的构造器、小时和分钟作为参数,然后返回一个符合ClockInterface
接口的新实例。
从这个例子可以看出,“类静态部分与实例部分的区别”主要体现在如何通过接口去约束类的构造器以及实例化之后的对象。静态部分(比如构造器)通常通过单独的接口来描述,而实例部分则是通过实现接口来保证类具有必要的结构和行为。
继承接口
在 TypeScript 中,接口(Interfaces)允许你定义一个对象应该有哪些形状,即它应该包含哪些属性和方法。继承接口是指一个接口可以扩展另一个接口的功能,就像类之间的继承一样。
- 接口继承允许你从一个接口复制成员到另一个接口,可以重用代码。
- 使用关键字
extends
来实现接口继承。 - 一个接口可以继承多个接口,通过逗号分隔每个接口。
下面是一些使用接口继承的例子:
// 定义一个基础接口
interface Shape {
color: string;
}
// 定义一个继承Shape接口的新接口
interface Square extends Shape {
sideLength: number;
}
// 这个对象拥有Shape和Square的所有属性
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
在上述代码中,Square
接口继承了 Shape
接口,因此一个 Square
类型的对象不仅需要有 sideLength
属性,还要有 Shape
接口中定义的 color
属性。
当一个接口继承多个接口时:
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
// 组合两个接口,创建了一个新的接口
interface Square extends Shape, PenStroke {
sideLength: number;
}
// 这个对象应具备Shape、PenStroke和Square的所有属性
let square = <Square>{};
square.color = "red";
square.sideWidth = 5.0; // 注意这里可能是个错误,应该是penWidth
square.sideLength = 7;
上面的 Square
接口同时继承了 Shape
和 PenStroke
接口,因此 Square
类型的对象必须包含 color
,penWidth
和 sideLength
属性。注意,如果有相同名称的属性,它们必须具有相同的类型;如果不同接口间有冲突,则会导致编译错误。
混合类型
在 TypeScript 中,一个接口可以定义一个对象的形状(shape),即它的属性和方法。但有时候,我们遇到的对象或结构不仅仅是简单的属性和方法的集合,它们可能以多种方式工作或互相结合。这就是所谓的混合类型(Hybrid Types)。混合类型让你能够表示那些同时兼具多个类型特征的实体。
混合类型的需要:在 JavaScript 中,函数可以有属性,对象可以有方法,函数也可以返回一个新的函数等。当你使用 TypeScript 时,描述这样的行为就需要用到混合类型。
如何定义混合类型接口:你可以通过在接口中同时定义多种类型标注(比如,既包括函数签名,又包括属性和其他类型的签名),来创建一个混合类型。
以下是一些例子:
// 定义一个混合类型
interface Counter {
// 函数类型部分
(start: number): string;
// 属性类型部分
interval: number;
// 方法类型部分
reset(): void;
}
// 使用混合类型
function getCounter(): Counter {
let counter = <Counter>function (start: number) {
return "Hello!";
}; // 初始化函数
counter.interval = 123; // 设置属性
counter.reset = function () {}; // 设置方法
return counter;
}
let c = getCounter();
c(10); // 调用函数部分
c.reset(); // 调用方法部分
c.interval = 5.0; // 访问属性部分
在上述例子中,Counter
接口定义了一个混合类型,它既是一个函数(接受一个number
类型的参数并返回一个string
),同时拥有一个名为interval
的number
类型属性和一个名为reset
的无参方法。函数getCounter
创建并返回了这样一个符合Counter
接口定义的对象。
混合类型非常灵活,能帮助你在 TypeScript 中更准确地描述 JavaScript 世界的各种复杂行为。
接口继承类
接口继承类是 TypeScript 中的一个高级概念,它允许你创建一个接口,这个接口继承自另一个类的成员,但不包括其实现。这意味着接口仅仅复制了类成员的类型,并非它们的实际实现。
以下是一些关键点和例子:
- 当接口继承类时,它会继承同一个类的所有成员,包括私有(
private
)和受保护(protected
)成员。 - 这样做的效果就好像接口声明了所有从类中继承来的成员,但没有提供具体实现。
- 任何实现该接口的类都必须匹配这些被继承的成员的类型。
- 这种方式主要用于当你有一个类,你想创建一个与其兼容的新对象,或者你在使用类但需要遵循特定的接口合约。
例子:
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() {}
}
class TextBox extends Control {
// 此处没有选择(select)方法,因此不能实现SelectableControl接口
}
class ImageControl implements SelectableControl {
private state: any; // 必须包含state,因为它继承自Control的私有成员
select() {}
}
上面的代码片段说明了以下几点:
Control
类有一个私有成员state
,它不可以在声明它的类的外部访问。SelectableControl
接口继承了Control
类,并添加了一个新的方法select()
。Button
类继承自Control
并实现了SelectableControl
接口,因此它必须实现select()
方法。TextBox
类继承自Control
类,但没有实现select()
方法,所以它不能实现SelectableControl
接口。ImageControl
类实现了SelectableControl
接口,尽管它并不是从Control
类继承而来的,但它必须声明一个state
属性,以满足接口对私有成员state
的要求。同时也必须实现select()
方法。
通过这种方式,TypeScript 允许你确保某些类不仅符合一个特定的结构,还显式地包含了某个类的成员。