一文带你了解TypeScript接口使用

本文概览:

在这里插入图片描述

1. 接口的基本用法

先看一下接口的定义:

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

TypeScript 的核心原则之一是对值所具有的结构进行类型检查,它有时被称做“鸭式辨型法”或“结构性子类型化”。在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

TypeScript 接口定义形式如下:

interface interface_name { }

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

const getFullName = ({ firstName, lastName }) => {
  return `${firstName} ${lastName}`;
};

调用时传入参数:

getFullName({
  firstName: "Hello",
  lastName: "TypeScript"
}); 
// 'Hello TypeScript'

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

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

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

const getFullName = ({
  firstName,
  lastName,
}: { // 指定这个参数的类型,因为他是一个对象,所以这里来指定对象中每个字段的类型
  firstName: string; // 指定属性名为firstName和lastName的字段的属性值必须为string类型
  lastName: string;
}) => {
  return `${firstName} ${lastName}`;
};

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

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

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

interface Info {
  firstName: string;
  lastName: string;
}
const getFullName = ({ firstName, lastName }: Info) =>
  `${firstName} ${lastName}`;

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

2. 接口的可选属性

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

定义一个描述传入蔬菜信息的句子的函数:

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

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

interface Vegetables {
  color?: string;
  type: string;
}

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

3. 接口的多余属性检查

对于上面的函数,我们这样来调用:

getVegetables({
  type: "tomato",
  size: "big"   // 'size'不在类型'Vegetables'中
});

这里没有写color属性,由于它是一个可选属性,所以是没有报错的。但是我们多写了一个size属性,这时就会报错,TypeScript就会告诉我们接口上不存在这个多余的属性,所以只要是接口上没有定义这个属性,在调用时出现了,就会报错。


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

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

(1) 使用类型断言

类型断言就是告诉 TypeScript,我们已经自行进行了检查,确保这个类型没有问题,希望 TypeScript 对此不进行检查,所以最简单的方式就是使用类型断言:

interface Vegetables {
  color?: string;
  type: string;
}
const getVegetables = ({ color, type }: Vegetables) => {
  return `A ${color ? color + " " : ""}${type}`;
};
getVegetables({
  type: "tomato",
  size: 12,
  price: 1.2
} as Vegetables);

(2) 添加索引签名

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

interface Vegetables {
  color: string;
  type: string;
  [prop: string]: any;
}
const getVegetables = ({ color, type }: Vegetables) => {
  return `A ${color ? color + " " : ""}${type}`;
};
getVegetables({
  color: "red",
  type: "tomato",
  size: 12,
  price: 1.2
});

(3) 利用类型兼容性

这种方法现在还不是很好理解,也是不推荐使用的,先来看写法:

interface Vegetables {
  type: string;
}
const getVegetables = ({ type }: Vegetables) => {
  return `A ${type}`;
};
const option = { type: "tomato", size: 12 };
getVegetables(option);

上面这种方法完美通过检查,我们将对象字面量赋给一个变量option,然后getVegetables传入 option,这时没有报错。是因为直接将对象字面量传入函数,和先赋给变量再将变量传入函数,这两种检查机制是不一样的,后者是因为类型兼容性。我们后面会有专门一节来讲类型兼容性。简单地来说:如果 b 要赋值给 a,那要求 b 至少需要与 a 有相同的属性,多了无所谓。

在上面这个例子中,option的类型应该是Vegetables类型,对象{ type: ‘tomato’, size: 12 }要赋值给 optionoption中所有的属性在这个对象字面量中都有,所以这个对象的类型和option(也就是Vegetables类型)是兼容的,所以上面例子不会报错。

4. 接口的只读属性

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

interface Role {
  readonly 0: string;
  readonly 1: string;
}

这里定义了一个角色字典,有 0 和 1 两种角色 id。下面我们定义一个实际的角色数据,然后来修改一下它的值:

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

这里TypeScript 告诉我们不能分配给索引0,因为它是只读属性。设置一个值只读,在ES6中,使用const定义的常量定义之后不能再修改,这有点只读的意思。那readonlyconst在使用时该如何选择呢?这主要看你这个值的用途,如果是定义一个常量,那用const,如果这个值是作为对象的属性,就用readonly

const NAME: string = "TypeScript";
NAME = "Haha"; // Uncaught TypeError: Assignment to constant variable
const obj = {
  name: "TypeScript"
};
obj.name = "Haha";

interface Info {
  readonly name: string;
}
const info: Info = {
  name: "TypeScript"
};
info["name"] = "Haha"; // Cannot assign to 'name' because it is a read-only property

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

5. 函数类型

接口不仅可以描述普通对象,还可以描述函数类型,我们先看写法:

interface AddFunc {
  (num1: number, num2: number): number;
}

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

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

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

6. 索引类型

可以使用接口描述索引的类型和通过索引得到的值的类型,比如一个数组[‘a’, ‘b’],数字索引0对应的通过索引得到的值为’a’。我们可以同时给索引和值都设置类型,看下面的示例:

interface RoleDic {
  [id: number]: string;
}
const role1: RoleDic = {
  0: "super_admin",
  1: "admin"
};
const role2: RoleDic = {
  s: "super_admin",  // error 不能将类型"{ s: string; a: string; }"分配给类型"RoleDic"。
  a: "admin"
};
const role3: RoleDic = ["super_admin", "admin"];

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

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

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

const obj = {
  123: "a", // 这里定义一个数值类型的123这个属性
  "123": "b" // 这里在定义一个字符串类型的123这个属性,这里会报错:标识符“"123"”重复。
};
console.log(obj); // { '123': 'b' }

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

7. 继承接口

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

interface Vegetables {
  color: string;
}
interface Tomato {
  color: string;
  radius: number;
}
interface Carrot {
  color: string;
  length: number;
}

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

interface Vegetables {
  color: string;
}
interface Tomato extends Vegetables {
  radius: number;
}
interface Carrot extends Vegetables {
  length: number;
}
const tomato: Tomato = {
  radius: 1.2 // error  Property 'color' is missing in type '{ radius: number; }'
};
const carrot: Carrot = {
  color: "orange",
  length: 20
};

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

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

interface Vegetables {
  color: string;
}
interface Food {
  type: string;
}
interface Tomato extends Food, Vegetables {
  radius: number;
}
const tomato: Tomato = {
  type: "vegetables",
  color: "red",
  radius: 1.2
};  
// 在定义tomato变量时将继承过来的color和type属性同时声明

8. 混合类型接口

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

let count = 0;
const countUp = () => count++;

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

// javascript
const countUp = (() => {
  let count = 0;
  return () => {
    return ++count;
  };
})();
console.log(countUp()); // 1
console.log(countUp()); // 2

在 TypeScript3.1 版本之前,我们需要借助命名空间来实现。但是在 3.1 版本,TypeScript 支持直接给函数添加属性,虽然这在 JavaScript 中早就支持了:

// javascript
let countUp = () => {
  return ++countUp.count;
};
countUp.count = 0;
console.log(countUp()); // 1
console.log(countUp()); // 2

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

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

interface Counter {
  (): void; // 这里定义Counter这个结构必须包含一个函数,函数的要求是无参数,返回值为void,即无返回值
  count: number; // 而且这个结构还必须包含一个名为count、值的类型为number类型的属性
}
const getCounter = (): Counter => { // 这里定义一个函数用来返回这个计数器
  const c = () => { // 定义一个函数,逻辑和前面例子的一样
    c.count++;
  };
  c.count = 0; // 再给这个函数添加一个count属性初始值为0
  return c; // 最后返回这个函数对象
};
const counter: Counter = getCounter(); // 通过getCounter函数得到这个计数器
counter();
console.log(counter.count); // 1
counter();
console.log(counter.count); // 2

上面的例子中,getCounter函数返回值类型为Counter,它是一个函数,无返回值,即返回值类型为void,它还包含一个属性count,属性返回值类型为number

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CUG-GZ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值