TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。即如果一个东西走起来像鸭子、叫起来像鸭子,那它就是鸭子。
通过定义接口,为特定的结构赋予了一个明确的名称和规范。
在 TypeScript 中,接口(Interface)用于定义对象的形状,即对象应该具有哪些属性以及这些属性的类型。
使用 interface 关键字来定义接口
interface Person {
name: string;
age: number;
}
定义了一个名为 Person
的接口:
name
:必需的属性,且类型为字符串string
。age
: 必需的属性,类型为数字number
。
实现接口
一个对象或类可以明确声明它实现了某个接口,以满足接口定义的属性要求。
// person1 合法
let person1: Person = {
name: "张三",
age: 30
};
// person2 不合法: 对象字面量只能指定已知属性,并且“likes”不在类型“Person”中。
let person2: Person = {
name: "李四",
age: 25,
likes: "dog"
};
TypeScript 的类型系统非常严格,将一个对象指定为某个特定接口的类型时,它必须仅包含该接口中定义的属性,不能多也不能少。
所以,由于 likes
属性不在 Person
接口的定义中,TypeScript 会认为对象 person2
的结构不符合 Person
接口的要求,从而导致编译错误。
可选属性
在属性名后添加 ?
来标记属性为可选的。
interface Person {
address?: string;
}
let person1: Person = {}; // 合法, 可以没有 address 属性
let person2: Person = {
address: "xxx街道xxx小区"
};
定义了一个名为 Person
的接口:
address
:可选属性,其类型为字符串string
。创建符合Person
接口的对象时,address
属性可有可无。
只读属性
在属性名前用 readonly
来指定只读属性。
interface Person {
readonly id: number;
}
// 类型 "{}" 中缺少属性 "id",但类型 "Person" 中需要该属性。
let person1: Person = {}; // Error: Property 'id' is missing in type '{}' but required in type 'Person'.
// 赋值后,id 的值就不能再改变了。
let person2: Person = {
id: 1,
};
person2.id = 2; // Error: 无法为“id”赋值,因为它是只读属性。
定义了一个名为 Person
的接口:
id
: 必需的属性,该属性只读。对象创建后,id
的值不能被重新赋值修改。
ReadonlyArray 类型用于表示一个只读的数组
创建 ReadonlyArray
后,就不能对其元素进行修改操作,例如添加、删除或修改元素的值:
let readonlyArr: ReadonlyArray<number> = [1, 2, 3];
// 以下操作会报错,因为不能修改只读数组的元素
// 类型“readonly number[]”中的索引签名仅允许读取。
readonlyArr[0] = 4;
// 类型“readonly number[]”上不存在属性“push”。
readonlyArr.push(4);
// 类型“readonly number[]”上不存在属性“pop”。
readonlyArr.pop();
// 无法为“length”赋值,因为它是只读属性。
readonlyArr.length = 10;
ReadonlyArray<T>
与 Array<T>
的区别:
Array<T>
是普通的可修改数组类型,允许进行添加、删除、修改元素等操作。
普通数组可以使用push
、pop
、splice
等方法来修改数组的内容。ReadonlyArray<T>
去掉了这些可变的方法,以确保数组在被使用时不会被意外修改。
不能将只读数组直接赋值给普通数组,类型不兼容:
let arr: Array<number> = [1, 2, 3];
let readonlyArr: ReadonlyArray<number> = [1, 2, 3];
// Error: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
// 类型 "readonly number[]" 为 "readonly",不能分配给可变类型 "number[]"。
arr = readonlyArr
可以使用类型断言强制的类型转换:
arr = readonlyArr as number[];
ReadonlyArray
会把常用于需要确保数组不被意外修改的场景,以增强代码的安全性和可预测性。
readonly
vs const
readonly
(用于接口中的属性):- 应用于对象的属性,表示该属性在对象创建后不能被重新赋值。
interface Person {
readonly name: string;
}
let person: Person = { name: "张三" };
// 无法为“name ”重新赋值,因为它是只读属性。
person.name = "李四";
let mutablePerson = person;
// 无法为“name ”重新赋值,因为它是只读属性。
mutablePerson.name = "李四";
把 person
赋值给了 mutablePerson
,mutablePerson
实际上和 person
指向的是同一个对象。
由于 Person
接口中定义了 name
为只读属性,所以无论是通过 person
还是 mutablePerson
来尝试修改 name
的值,都是不被允许的,都会触发类型检查错误,提示不能对只读属性进行重新赋值。
这体现了 TypeScript 对接口中只读属性的严格类型检查,确保了在任何对该对象的操作中,都遵循了只读属性的约束。
const
- const声明一个只读的常量。一旦声明,常量的值就不能改变
- 对于基本数据类型(如数字、字符串、布尔值等),
const
声明的变量的值是不可变的。 - 对于引用数据类型(如对象、数组等),
const
只是保证变量的引用地址不能改变,但可以修改引用对象的内部属性。
const num = 5;
// 报错,不能重新给 const 基本类型变量赋值
num = 6; // Cannot assign to 'num' because it is a constant.
const person = { name: "张三" };
person.name = "李四"; // 可以修改name 属性,因为person对象的引用地址没有改变
person = { name: "李四" }; // 报错,不能重新给 const 引用类型变量 重新 赋值 引用地址
额外属性检查
定义了一个函数 printPersonInfo
,它接受一个参数 infoObj
,其类型为 Person
:
interface Person {
name: string;
age?: number;
}
function printPersonInfo(infoObj: Person) {
console.log(infoObj);
}
// Object literal may only specify known properties, and 'likes' does not exist in type 'Person'.
// 对象字面量只能指定已知属性,并且“likes”不在类型“Person”中。
printPersonInfo({ name: "李四", age: 25, likes: "dog" }); // 报错
调用printPersonInfo()
时,传入方法的参数 具有name
属性,满足 Person
接口的部分要求。age
是可选参数,可要可不要。有一个额外的likes
属性。
为什么会报错??
参数是以 对象字面量 的形式直接传递给printPersonInfo()
方法。
当把对象字面量赋值给变量或者作为参数传递的时候,对象字面量会被特殊对待而且会经过 额外属性检查。 如果一个对象字面量存在任何“目标类型”不包含的属性时,会编译错误。
如何绕开检查
- 使用类型断言绕开检查:
printPersonInfo({ name: "李四", age: 25, likes: "dog" } as Person);
- 添加索引签名
[propName: string]:any
:
interface Person {
name: string;
age?: number;
[propName: string]:any;
}
[propName: string]: any
是一个索引签名。它表示可以有任意数量的额外属性,属性名是字符串类型,属性值可以是任何类型。
这样的接口定义提供了一定的灵活性。既规定了一些明确的必需和可选属性(name
和 age
),又允许对象具有其他任意的属性。
- 将这个对象赋值给一个另一个变量: 因为
myObj
不会经过额外属性检查,所以编译器不会报错。
let myObj = { name: "李四", age: 25, likes: "dog" }; // 参数合法
printPersonInfo(myObj);
对象 myObj
有额外的 likes
属性,为什么不报错?
因为对象 myObj
本身是一个普通对象, 它没有类型,不会经过额外属性检查。在作为参数传递时,myObj
具有 Person
接口要求的 name
、 age
属性,所以可以将 myObj
作为参数传递给 printPersonInfo
函数。
在 TypeScript 中,当将一个对象作为参数传递给一个函数,并且函数期望的参数类型是一个接口时,如果该对象包含了接口中定义的必要属性,即使它还具有额外的属性,也会被认为是合法的传递。
这体现了 TypeScript 基于结构的类型检查原则,只要对象满足接口定义的必要属性,就可以被视为该接口类型的对象。
函数类型
接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
定义一个名为 MathOperation
的接口,它表示一个接受两个数字类型的参数 x
和 y
,并返回一个数字的函数类型:
interface MathOperation {
(x: number, y: number): number;
}
我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。
let sum: MathOperation;
sum = function(x: number, y: number): number{
return x + y
}
// 简洁写法:let sum: MathOperation = (x, y) => x + y;
console.log( sum(10, 15) ); // 25
声明变量 sum
,其类型为 MathOperation
。
接下来,将一个匿名函数赋值给 sum
。这个匿名函数接受两个数字参数 x
和 y
,并返回它们的和,符合 MathOperation
接口定义的函数结构。
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。
参数 x
和 y
可以用其他变量代替。
let sum: MathOperation;
sum = function(a: number, b: number): number{
return a + b
}
函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了 MathOperation
类型变量。 函数的返回值类型是通过其返回值推断出来的。
let sum: MathOperation;
sum = function(x, y){
return x + y
}
可索引的类型
可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。
示例:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0]; // Bob
定义StringArray
接口,它具有索引签名。 这个索引签名表示了当用 number
去索引StringArray
时会得到string
类型的返回值。
TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number
来索引时,JavaScript会将它转换成 string
然后再去索引对象。 也就是说用 100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
在这段代码中,定义的 NotOkay
接口会导致错误。
因为在一个接口中同时定义了数字索引类型为 Animal
和字符串索引类型为 Dog
。
当使用索引来访问对象时:
- 使用数字索引,期望得到的是
Animal
类型; - 使用字符串索引,期望得到的是
Dog
类型。
但在实际情况中,这样的混合定义可能会导致混乱和不一致,因为无法明确在特定索引下应该返回哪种确切的类型。
这违反了 TypeScript 对于类型定义的一致性和明确性原则。
字符串索引签名能够很好的描述dictionary
模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property
和 obj["property"]
两种形式都可以。
示例中, name
的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
interface NumberDictionary {
[index: string]: number;
length: number; // 可以,length是number类型
name: string // 错误,`name`的类型与索引类型返回值的类型不匹配
}
[index: string]: number;
这表示通过字符串索引访问这个对象时,返回的值应该是数字类型。
因为按照接口的定义,所有属性的值都应该是数字类型,但 name
的类型被定义为 string
,与索引返回值的类型不匹配。
将索引签名设置为只读,防止给索引赋值:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // 类型“ReadonlyStringArray”中的索引签名仅允许读取。
类类型
在 TypeScript 中,“类类型”指的是使用 class
关键字定义的一种结构,用于描述具有特定属性、方法和行为的对象的模板。
类类型可以包含以下主要元素:
- 属性:描述对象的数据成员,具有特定的类型。
- 构造函数:用于初始化对象的属性,在创建对象实例时被调用。
- 方法:定义对象可以执行的操作。
implements
关键字
在 TypeScript 中,implements
关键字用于让一个类实现一个或多个接口。
当一个类使用 implements
关键字后跟一个接口名称时,它必须满足该接口所定义的所有属性和方法的要求。
这意味着:
- 类必须具有接口中定义的所有属性,并且属性的类型必须完全匹配。
- 类必须实现接口中定义的所有方法,方法的名称、参数列表和返回值类型都要与接口中的定义一致。
示例:
interface MyInterface {
method1(): void;
property1: string;
}
class MyClass implements MyInterface {
property1: string = "value";
method1(): void {
// 方法的实现
}
}
在上述示例中,MyClass
类实现了 MyInterface
接口。它具有与接口中定义相同的属性 property1
和方法 method1
,并且属性的类型和方法的签名都符合接口的要求。
类静态部分与实例部分的区别
类是具有两个类型的:静态部分的类型和实例的类型。
定义一个名为 ClockConstructor
的接口:
interface ClockConstructor {
new (hour: number, minute: number);
}
ClockConstructor
接口定义了一个构造函数的签名,要求实现这个接口的类必须具有一个接受两个参数(一个数字类型的 hour
和一个数字类型的 minute
)的构造函数。
然后定义了 Clock
类实现 ClockConstructor
接口:
class Clock implements ClockConstructor {
constructor(h: number, m: number) { }
}
理论上:Clock
类的构造函数 constructor(h: number, m: number)
符合 ClockConstructor
接口中定义的构造函数要求,所以实现了该接口。
实际上:报错了!!
因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor
存在于类的静态部分,所以不在检查的范围内。
因此,需要直接操作类的静态部分。定义两个接口: ClockConstructor
为构造函数所用和ClockInterface为实例方法所用
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick(): any;
}
class Clock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
// 实例化
let myClock = new Clock(5, 30);
myClock.tick(); // beep beep
ClockConstructor
接口定义了一个构造函数的签名。ClockConstructor
接口规定了任何实现这个接口的构造函数,都必须接受两个数字类型的参数,并创建出符合ClockInterface
接口的对象。new
关键字表明这是在描述一个构造函数。(hour: number, minute: number)
是构造函数的参数列表。这表示该构造函数应接受两个number
类型的参数hour
和minute
。: ClockInterface
是构造函数的返回值类型。当使用这个构造函数创建对象时,所创建的对象应该符合ClockInterface
接口的定义。这就对通过这个构造函数创建的对象施加了一种约束,要求它们具有ClockInterface
中规定的属性和方法。- 这样定义的目的是为了统一规定创建时钟类的构造函数的参数和创建出的对象的类型。
ClockInterface
接口定义了一个方法tick()
,表示实现该接口的类需要具有这个方法。- 这意味着任何实现这个接口的类都需要有这个方法。
- 定义
Clock
类 实现ClockInterface
接口:Clock
类必须提供tick
方法。Clock
类的构造函数constructor(h: number, m: number)
符合ClockConstructor
接口中定义的构造函数要求。
注意:接口中定义的构造函数的返回值类型应该是一个具体的类型。
如果希望表示构造函数创建的对象不返回任何有意义的值,可以将返回值类型指定为 void
;如果希望指定创建的对象的类型,应该是一个具体的类或接口类型。
// 表示构造函数创建的对象不返回任何有意义的值,将返回值类型指定为 `void`
interface ClockConstructor {
new (hour: number, minute: number): void;
}
// 返回值是一个具体的类型
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface; // 假设 ClockInterface 是已定义的接口
}
接口继承接口
一个接口可以继承自另一个接口,从而扩展其属性和方法;可以更灵活地将接口分割到可重用的模块里。
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
salary: number;
}
let Employee1: Employee = {
name: "张三",
age: 30,
salary: 3000
};
一个接口可以继承多个接口,创建出多个接口的合成接口。
// Shape 接口定义了一个计算面积的方法 area
interface Shape {
area(): number;
}
// Colorable 接口定义了一个表示颜色的属性 color
interface Colorable {
color: string;
}
// Square 接口继承了 Shape 和 Colorable 接口,并且添加了一个表示边长的属性 sideLength 。
interface Square extends Shape, Colorable {
sideLength: number;
}
// MySquare 类 实现了 Square 接口
class MySquare implements Square {
sideLength: number;
color: string;
constructor(sideLength: number, color: string) {
this.sideLength = sideLength;
this.color = color;
}
area(): number {
return this.sideLength * this.sideLength;
}
}
let mySquare = new MySquare(5, "red");
console.log(mySquare.area()); // 25
console.log(mySquare.color); // red
Square
接口是包含了 Shape
和 Colorable
接口的特性的合成接口。
实现 Square
接口的类需要同时满足这三个接口的要求。
接口继承类
在 TypeScript 中,接口可以继承类。
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private
和protected
成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
示例:
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
distanceFromOrigin(): number {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
}
interface Point3D extends Point {
z: number;
}
class MyPoint3D implements Point3D {
x: number;
y: number;
z: number;
constructor(x: number, y: number, z: number) {
this.x = x;
this.y = y;
this.z = z;
}
distanceFromOrigin(): number {
// 重写父类的方法
console.log(this.x, this.y, this.z);
const distanceSquared = this.x * this.x + this.y * this.y + this.z * this.z;
return Math.sqrt(distanceSquared);
}
}
let myP = new MyPoint3D(10, 20, 30);
console.log( myP.distanceFromOrigin() );
Point
类具有x
、y
属性和distanceFromOrigin
方法。Point3D
接口继承了Point
类。- 继承属性:接口
Point3D
继承了类Point
的属性x
和y
的声明。 - 不继承实现:接口
Point3D
继承了类Point
的属性和方法的声明,但它并没有继承类中方法的具体实现。比如,distanceFromOrigin
方法的实现不会被继承,实现Point3D
接口的类需要自己提供这个方法的实现(或者选择不实现,如果不是必须的)。 - 新增属性:在继承的基础上,接口
Point3D
增加了新的属性z
。
- 继承属性:接口
MyPoint3D
类实现了Point3D
接口。在MyPoint3D
类中:- 必须明确地实现接口中继承的属性
x
、y
以及新增的属性z
。 - 由于接口
Point3D
没有继承方法的实现,所以MyPoint3D
类需要提供distanceFromOrigin
方法的实现。
- 必须明确地实现接口中继承的属性
混合类型
在 TypeScript 中,混合类型(Hybrid Type)是指一个对象既具有属性又具有方法,同时还可能表现出一些函数的特征。
示例:
interface Counter {
count: number;
(): number;
increment(): void;
}
// 声明变量counter,并指定它的类型是 Counter接口
let counter: Counter = {
count: 0,
increment() {
this.count++;
},
// 作为函数被调用时返回当前计数
() {
return this.count;
}
};
counter.increment();
console.log(counter());
声明变量 counter
,并指定其类型为 Counter
接口类型。这意味着:
counter
变量必须具有符合 Counter
接口定义的结构和行为。它需要有一个数值类型的属性 count
,一个无返回值的方法 increment
,以及一个能返回数值的函数调用形式 ()