基础篇-接口类型-《前端知识进阶》

admin 2025-11-01 15:35:36 编程 来源:ZONE.CI 全球网 0 阅读模式
  • 一、接口定义
  • 二、接口属性
    • 1. 可选属性
    • 2. 只读属性
    • 3. 多余属性检查
      • (1) 使用类型断言
      • (2) 添加索引签名
  • 三、接口使用
    • 1. 定义函数类型
    • 2. 定义索引类型
  • 四、高级用法
    • 1. 继承接口
    • 2. 混合类型接口
  • 五、类型别名
    • 1. 基本使用
    • 2. 与接口区别

    在JavaScript中,我们似乎很少听说接口这个概念,这是TypeScript中很常用的一个特性,它让 TypeScript 具备了 JavaScript 所缺少的、描述较为复杂数据结构的能力。下面就来看看什么是接口类型。

    一、接口定义

    接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。

    TypeScript 的核心原则之一是对值所具有的结构进行类型检查,并且只要两个对象的结构一致,属性和方法的类型一致,则它们的类型就是一致的。**在TypeScript里,接口的作用就是为这些类型命名和为代码或第三方代码定义契约。**

    TypeScript 接口定义形式如下:

    1. interface interface_name { }

    来看例子,函数的参数是一个对象,它包含两个字段:firstName 和 lastName,返回一个拼接后的完整名字:

    1. const getFullName = ({ firstName, lastName }) => {
    2. return `${firstName} ${lastName}`;
    3. };

    调用时传入参数:

    1. getFullName({
    2. firstName: "Hello",
    3. lastName: "TypeScript"
    4. });

    这样调用是没有问题的,但是如果传入的参数不是想要的参数格式时,就会出现一些错误:

    1. getFullName(); // Uncaught TypeError: Cannot destructure property `a` of 'undefined' or 'null'.
    2. getFullName({ age: 18, phone: 110 }); // 'undefined undefined'
    3. getFullName({ firstName: "Hello" }); // 'Hello undefined'

    这些都是我们不想要的,在开发时难免会传入错误的参数,所以 TypeScript 能够在编译阶段就检测到这些错误。下面来完善下这个函数的定义:

    1. const getFullName = ({
    2. firstName,
    3. lastName,
    4. }: {
    5. firstName: string; // 指定属性名为firstName和lastName的字段的属性值必须为string类型
    6. lastName: string;
    7. }) => {
    8. return `${firstName} ${lastName}`;
    9. };

    通过对象字面量的形式去限定传入的这个对象的结构,现在再来看下之前的调用会出现什么提示:

    1. getFullName(); // 应有1个参数,但获得0个
    2. getFullName({ age: 18, phone: 110 }); // 类型“{ age: number; phone: number; }”的参数不能赋给类型“{ firstName: string; lastName: string; }”的参数。
    3. getFullName({ firstName: "Hello" }); // 缺少必要属性lastName

    这些都是在编写代码时 TypeScript 提示的错误信息,这样就避免了在使用函数的时候传入不正确的参数。我们可以使用interface来定义接口:

    1. interface Info {
    2. firstName: string;
    3. lastName: string;
    4. }
    5. const getFullName = ({ firstName, lastName }: Info) =>
    6. return `${firstName} ${lastName}`;
    7. };

    注意:在定义接口时,不要把它理解为是在定义一个对象,{}括号包裹的是一个代码块,里面是声明语句,只不过声明的不是变量的值而是类型。声明也不用等号赋值,而是冒号指定类型。每条声明之前用换行分隔即可,也可以使用分号或者逗号。

    二、接口属性

    1. 可选属性

    在定义一些结构时,一些结构的某些字段的要求是可选的,有这个字段就做处理,没有就忽略,所以针对这种情况,TypeScript提供了可选属性。

    定义一个函数:

    1. const getVegetables = ({ color, type }) => {
    2. return `A ${color ? color + " " : ""}${type}`;
    3. };

    这个函数中根据传入对象中的 color 和 type 来进行描述返回一句话,color 是可选的,所以可以给接口设置可选属性,在属性名后面加个?即可:

    1. interface Vegetables {
    2. color?: string;
    3. type: string;
    4. }
    5. const getVegetables = ({ color, type }: Vegetables) => {
    6. return `A ${color ? color + " " : ""}${type}`;
    7. };

    这里可能会报一个警告:接口应该以大写的i开头,可以在 tslint.json 的 rules 里添加"interface-name": [true, “never-prefix”]来关闭这条规则。

    当属性被标注为可选后,它的类型就变成了显式指定的类型与 undefined 类型组成的联合类型,比如 getVegetables 方法的参数中的 color 属性类型就变成了这样:

    1. string | undefined;

    那下面的接口与上述接口是一样的吗?

    1. interface Vegetables2 {
    2. color?: string | undefined;
    3. type: string;
    4. }

    答案肯定是否定的,因为可选意味着可以不设置属性键名,类型是 undefined 意味着属性键名不可选。

    2. 只读属性

    接口可以设置只读属性,如下:

    1. interface Role {
    2. readonly 0: string;
    3. readonly 1: string;
    4. }

    这里定义了一个角色,有 0 和 1 两种角色 id。下面定义一个角色数据,并修改一下它的值:

    1. const role: Role = {
    2. 0: "super_admin",
    3. 1: "admin"
    4. };
    5. role[1] = "super_admin"; // Cannot assign to '0' because it is a read-only property

    这里TypeScript 提示不能分配给索引0,因为它是只读属性。

    在ES6中,使用const定义的常量定义之后不能再修改,这和只读意思接近。那readonlyconst在使用时该如何选择呢?那主要看这个值的用途,如果是定义一个常量,那用const,如果这个值是作为对象的属性,就用readonly

    1. const NAME: string = "TypeScript";
    2. NAME = "Haha"; // Uncaught TypeError: Assignment to constant variable
    3. const obj = {
    4. name: "TypeScript"
    5. };
    6. obj.name = "Haha";
    7. interface Info {
    8. readonly name: string;
    9. }
    10. const info: Info = {
    11. name: "TypeScript"
    12. };
    13. info["name"] = "Haha"; // Cannot assign to 'name' because it is a read-only property

    上面使用const定义的常量NAME定义之后再修改会报错,但是如果使用const定义一个对象,然后修改对象里属性的值是不会报错的。所以如果要保证对象的属性值不可修改,需要使用readonly

    需要注意,readonly只是静态类型检测层面的只读,实际上并不能阻止对对象的修改。因为在转译为 JavaScript 之后,readonly 修饰符会被抹除。因此,任何时候与其直接修改一个对象,不如返回一个新的对象,这是一种比较安全的方式。

    3. 多余属性检查

    先来看下面的例子:

    1. interface Vegetables {
    2. color?: string;
    3. type: string;
    4. }
    5. const getVegetables = ({ color, type }: Vegetables) => {
    6. return `A ${color ? color + " " : ""}${type}`;
    7. };
    8. getVegetables({
    9. type: "tomato",
    10. size: "big" // 'size'不在类型'Vegetables'中
    11. });

    这里没有传入 color 属性,因为它是一个可选属性,所以没有报错。但是多传入了一个 size 属性,这时就会报错,TypeScript就会提示接口上不存在这个多余的属性,所以只要是接口上没有定义这个属性,在调用时出现了,就会报错。

    注意:这里可能会报一个警告:属性名没有按开头字母顺序排列属性列表,可以在 tslint.json 的 rules 里添加"object-literal-sort-keys": [false]来关闭这条规则。

    有时 不希望TypeScript这么严格的对数据进行检查,比如上面的函数,只需要保证传入getVegetables的对象有type属性就可以了,至于实际使用的时候传入对象有没有多余的属性,多余属性的属性值是什么类型,我们就不管了,那就需要绕开多余属性检查,有如下三个方法:

    (1) 使用类型断言

    类型断言就是告诉 TypeScript,已经自行进行了检查,确保这个类型没有问题,希望 TypeScript 对此不进行检查,这是最简单的实现方式,类型断言使用 as 关键字来定义(这里不细说,后面进阶篇会专门介绍类型断言):

    1. interface Vegetables {
    2. color?: string;
    3. type: string;
    4. }
    5. const getVegetables = ({ color, type }: Vegetables) => {
    6. return `A ${color ? color + " " : ""}${type}`;
    7. };
    8. getVegetables({
    9. type: "tomato",
    10. size: 12,
    11. price: 1.2
    12. } as Vegetables);

    (2) 添加索引签名

    更好的方式是添加字符串索引签名:

    1. interface Vegetables {
    2. color: string;
    3. type: string;
    4. [prop: string]: any;
    5. }
    6. const getVegetables = ({ color, type }: Vegetables) => {
    7. return `A ${color ? color + " " : ""}${type}`;
    8. };
    9. getVegetables({
    10. color: "red",
    11. type: "tomato",
    12. size: 12,
    13. price: 1.2
    14. });

    三、接口使用

    1. 定义函数类型

    在前面函数类型篇我们说了,可以使用接口来定义函数类型:

    1. interface AddFunc {
    2. (num1: number, num2: number): number;
    3. }

    这里定义了一个AddFunc结构,这个结构要求实现这个结构的值,必须包含一个和结构里定义的函数一样参数、一样返回值的方法,或者这个值就是符合这个函数要求的函数。把花括号里包着的内容称为调用签名,它由带有参数类型的参数列表和返回值类型组成:

    1. const add: AddFunc = (n1, n2) => n1 + n2;
    2. const join: AddFunc = (n1, n2) => `${n1} ${n2}`; // 不能将类型'string'分配给类型'number'
    3. add("a", 2); // 类型'string'的参数不能赋给类型'number'的参数

    上面定义的add函数接收两个数值类型的参数,返回的结果也是数值类型,所以没有问题。而join函数参数类型没错,但是返回的是字符串,所以会报错。而当调用add函数时,传入的参数如果和接口定义的类型不一致,也会报错。在实际定义函数的时候,名字是无需和接口中参数名相同的,只需要位置对应即可。

    实际上,很少使用接口类型来定义函数类型,更多使用内联类型或类型别名配合箭头函数语法来定义函数类型:

    1. type AddFunc = (num1: number, num2: number) => number;

    这里给箭头函数类型指定了一个别名 AddFunc,在其他地方就可以直接复用 AddFunc,而不用重新声明新的箭头函数类型定义。

    2. 定义索引类型

    在实际工作中,使用接口类型较多的地方是对象,比如 React 组件的 Props & State等,这些对象有一个共性,即所有的属性名、方法名都是确定的。

    实际上,经常会把对象当 Map 映射使用,比如下边代码中定义了索引是任意数字的对象 role1 和索引是任意字符串的对象 role2。

    1. const role1 = {
    2. 0: "super_admin",
    3. 1: "admin"
    4. };
    5. const role2 = {
    6. s: "super_admin",
    7. a: "admin"
    8. };

    这时需要使用索引签名来定义上边提到的对象映射结构,并通过 “[索引名: 类型]”的格式约束索引的类型。索引名称的类型分为 string 和 number 两种,通过如下定义的 RoleDic 和 RoleDic1 两个接口,可以用来描述索引是任意数字或任意字符串的对象:

    1. interface RoleDic {
    2. [id: number]: string;
    3. }
    4. interface RoleDic1 {
    5. [id: string]: string;
    6. }
    7. const role1: RoleDic = {
    8. 0: "super_admin",
    9. 1: "admin"
    10. };
    11. const role2: RoleDic = {
    12. s: "super_admin", // error 不能将类型"{ s: string; a: string; }"分配给类型"RoleDic"。
    13. a: "admin"
    14. };
    15. const role3: RoleDic = ["super_admin", "admin"];

    需要注意,当使用数字作为对象索引时,它的类型既可以与数字兼容,也可以与字符串兼容,这与 JavaScript 的行为一致。因此,使用 0 或 ‘0’ 索引对象时,这两者等价。

    上面的 role3 定义了一个数组,索引为数值类型,值为字符串类型。我们还可以给索引设置readonly,从而防止索引返回值被修改:

    1. interface RoleDic {
    2. readonly [id: number]: string;
    3. }
    4. const role: RoleDic = {
    5. 0: "super_admin"
    6. };
    7. role[0] = "admin"; // error 类型"RoleDic"中的索引签名仅允许读取

    注意,可以设置索引类型为 number。但是这样如果将属性名设置为字符串类型,则会报错;但是如果设置索引类型为字符串类型,那么即便属性名设置的是数值类型,也没问题。因为 JS 在访问属性值时,如果属性名是数值类型,会先将数值类型转为字符串,然后再去访问:

    1. const obj = {
    2. 123: "a",
    3. "123": "b" // 报错:标识符“"123"”重复。
    4. };
    5. console.log(obj); // { '123': 'b' }

    如果数值类型的属性名不会转为字符串类型,那么这里数值123和字符串123是不同的两个值,则最后对象obj应该同时有这两个属性;但是实际打印出来的obj只有一个属性,属性名为字符串”123”,值为”b”,说明数值类型属性名123被覆盖掉了,就是因为它被转为了字符串类型属性名”123”;又因为一个对象中多个相同属性名的属性,定义在后面的会覆盖前面的,所以结果就是obj只保留了后面定义的属性值。

    四、高级用法

    1. 继承接口

    在 TypeScript 中,接口是可以继承,这和ES6中的类一样,这提高了接口的可复用性。来看一个场景:定义一个Vegetables接口,它会对color属性进行限制。再定义两个接口TomatoCarrot,这两个类都需要对color进行限制,而各自又有各自独有的属性限制,可以这样定义:

    1. interface Vegetables {
    2. color: string;
    3. }
    4. interface Tomato {
    5. color: string;
    6. radius: number;
    7. }
    8. interface Carrot {
    9. color: string;
    10. length: number;
    11. }

    三个接口中都有对color的定义,但是这样写很繁琐,可以用继承来改写:

    1. interface Vegetables {
    2. color: string;
    3. }
    4. interface Tomato extends Vegetables {
    5. radius: number;
    6. }
    7. interface Carrot extends Vegetables {
    8. length: number;
    9. }
    10. const tomato: Tomato = {
    11. radius: 1.2 // error Property 'color' is missing in type '{ radius: number; }'
    12. };
    13. const carrot: Carrot = {
    14. color: "orange",
    15. length: 20
    16. };

    上面定义的 tomato 变量因为缺少了从Vegetables接口继承来的 color 属性,所以报错了。

    一个接口可以被多个接口继承,同样,一个接口也可以继承多个接口,多个接口用逗号隔开。比如再定义一个Food接口,Tomato 也可以继承 Food

    1. interface Vegetables {
    2. color: string;
    3. }
    4. interface Food {
    5. type: string;
    6. }
    7. interface Tomato extends Food, Vegetables {
    8. radius: number;
    9. }
    10. const tomato: Tomato = {
    11. type: "vegetables",
    12. color: "red",
    13. radius: 1
    14. };

    如果想要覆盖掉继承的属性,那就只能使用兼容的类型进行覆盖:

    1. interface Tomato extends Vegetables {
    2. color: number;
    3. }

    这里我们将color属性进行了覆盖,并将其类型设置为了number类型,这时就会报错,因为Tomato 和 Vegetables 中的name属性是不兼容的。

    2. 混合类型接口

    在 JavaScript 中,函数是对象类型。对象可以有属性,所以有时一个对象既是一个函数,也包含一些属性。比如要实现一个计数器函数,比较直接的做法是定义一个函数和一个全局变量:

    1. let count = 0;
    2. const counter = () => count++;

    但是这种方法需要在函数外面定义一个变量,更优一点的方法是使用闭包:

    1. const counter = (() => {
    2. let count = 0;
    3. return () => {
    4. return count++;
    5. };
    6. })();
    7. console.log(counter()); // 1
    8. console.log(counter()); // 2

    TypeScript 支持直接给函数添加属性,这在 JavaScript 中是支持的:

    1. let counter = () => {
    2. return counter.count++;
    3. };
    4. counter.count = 0;
    5. console.log(counter()); // 1
    6. console.log(counter()); // 2

    这里把一个函数赋值给countUp,又给它绑定了一个属性count,计数保存在这个 count 属性中。

    可以使用混合类型接口来指定上面例子中 counter 的类型:

    1. interface Counter {
    2. (): void;
    3. count: number;
    4. }
    5. const getCounter = (): Counter => {
    6. const c = () => {
    7. c.count++;
    8. };
    9. c.count = 0;
    10. return c;
    11. };
    12. const counter: Counter = getCounter();
    13. counter();
    14. console.log(counter.count); // 1
    15. counter();
    16. console.log(counter.count); // 2

    这里定义了一个Counter接口,这个结构必须包含一个函数,函数的要求是无参数,返回值为void,即无返回值。而且这个结构还必须包含一个名为count、值的类型为number类型的属性。最后,通过getCounter函数得到这个计数器。这里 getCounter 的类型为Counter,它是一个函数,无返回值,即返回值类型为void,它还包含一个属性count,属性返回值类型为number

    五、类型别名

    类型别名并不属于接口类型的内容,但是它和接口功能类似,所以这里放在一起来说。

    1. 基本使用

    接口类型的作用就是将内联类型抽离出来,从而实现类型复用。其实,还可以使用类型别名接收抽离出来的内联类型实现复用。可以通过如下所示“type 别名名字 = 类型定义”的格式来定义类型别名,比如上面定义的计数器方法的类型:

    1. type Counter = {
    2. (): void;
    3. count: number;
    4. }

    这样写看起来就像是在JavaScript中定义变量,只不过是把var、let、const换成了type。实际上,类型别名可以在接口类型无法覆盖的场景中使用,比如联合类型、交叉类型等:

    1. // 联合类型
    2. type Name = number | string;
    3. // 交叉类型
    4. type Vegetables = {color: string, radius: number} & {color: string, length: number}

    这里定义了一个 Vegetables 类型别名,表示两个匿名接口类型交叉出的类型。

    需要注意:类型别名只是给类型取了一个别名,并不是创建了一个新的类型。

    2. 与接口区别

    通过上面的介绍,可以发现多数情况下是可以使用类型别名来替代的,那这是否说明这两者是等价的呢?答案肯定是否定的,不然也不会出这两个概念。在某些特定的场景下,这两者还是存在很大区别。比如,重复定义的接口类型,它的属性会叠加,这个特性使得我们可以很方便地对全局变量、第三方库的类型做扩展:

    1. interface Vegetables {
    2. color: string;
    3. }
    4. interface Vegetables {
    5. radius: number;
    6. }
    7. interface Vegetables {
    8. length: number;
    9. }
    10. let vegetables: Vegetables = {
    11. color: "red",
    12. radius: 2,
    13. length: 10
    14. }

    这里我们定义了三次 Vegetables 接口,此时可以赋值给变一个包含color、radius、length的对象,并且不会报错。

    如果重复定义类型别名:

    1. type Vegetables = {
    2. color: string;
    3. }
    4. type Vegetables = {
    5. radius: number;
    6. }
    7. type Vegetables = {
    8. length: number;
    9. }
    10. let vegetables: Vegetables = {
    11. color: "red",
    12. radius: 2,
    13. length: 10
    14. }

    上述代码就会报错:’Vegetables’ is already defined。所以,接口类型是可重复定义且属性会叠加的,而类型别名是不可重复定义的。

    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  5