本文是在 王亦斯 的 2 篇文章基础上,整理和补充而来:
巧用 TypeScript:https://zhuanlan.zhihu.com/p/39620591
巧用 TypeScript (二):https://zhuanlan.zhihu.com/p/64423022
我会按照他的原文顺序来逐个重新过一遍,但是在中间会根据自己实际运行结果,补充或添加自己的使用建议。
本文中的命名约定:
- 凡是 interface 定义的,格式都为 I + Xxxx,例如 IPerson
- 凡是 type 定义的,格式都为 Xxxx + Type,例如 PersonType
- 泛型:使用<> 来包裹
- 泛型约束:使用泛型来约束类型,有非常多不同的使用方法,例如 type PersonType<T> = T ; 约束返回值的类型必须等同于T
- TypeScript内置的映射类型:Pick 、Partial、Required、Readonly、Record、Omit、Exclude、Extract、NonNullable、ReturnType、InstanceType
- keyof:获取 interface 定义中的属性名
- typeof:推导出 变量或实例对象对应的 type类型
- in:只允许在 type类型内部使用,用来获取 type类型的属性名
- infer:在 extends 条件推断中 待推断类型的变量,(我暂时没明白 infer 的用法)
在TypeScript中,通过添加多行注释,可以给注释对象添加友好的提示信息,例如:
/** 定义Person的接口 */
interface IPerson {
name:string
}
只能是多行注释,单行注释则不无法提供提示信息 补充:在VSCode中设置多行注释的快捷键为 shift + alt + a、或者安装插件 koroFileHeader,对应添加快捷键为 ctrl + alt + t
在 TypeScript 的注释中,可以添加遵循 JSDoc 注释规范的关键词。 在多行注释中,输入 @ 即可出现支持的关键词,例如 @description 表示描述、@param 表示参数、@return 表示返回值、@example 表示使用示例,等等。
/**
* @description 定义Person的接口
*/
interface IPerson {
name:string
}
通常情况下,我们会先定义类型,然后再定义实例,例如:
interface IPerson {
name: string,
age: number
}
const person: IPerson = {
name: 'puxiao',
age: 34
}
上面是标准正确的写法,没有任何问题,但是假设我们希望偷懒,少写一些代码,那么可以这样做:先定义实例 person,然后通过 typeof 让 TypeScript 推导 出类型。
const person = {
name: 'puxiao',
age: 34
}
type PersonType = typeof person
在 TypeScript 中,typeof 可以获取变量或对象的类型,得到的类型是由 TypeScript 推导(推理) 出来的,可以是任何结构形式的类型 在 JavaScript 中,typeof 可以获取变量或对象的类型,只不过得到的类型结果只能是:undefined、string、number、boolean、symbol、object、function
特别提醒:
由于是从实例(变量或对象) 反向推理 出对应类型,而实例中的某属性一定是固定类型的,所以请看下面代码:
interface IPerson {
age: string | number
}
const person = {
age: '34岁'
}
type PersonType = typeof person
上面代码中,由于 person.age 为字符串,所以 PersonType 实际结果为:
type PersonType = {
age: string;
}
IPerson 和 PersonType 是不相同的,所以请注意:typeof 是无法满足、推理出属性中有联合类型的情况。
这一小节,我认为原文写的并不对,我完全按照自己的逻辑来重写一遍。
假设 person 有一个属性 age,该属性可能为字符串 '34岁'(string) 或者为数字 34(number),那么在定义 IPerson 时,我们很容易想到:
interface IPerson {
age: string | number
}
另外一种情况,假设 person 的属性可能有 boyName 或 grilName,且只能同时存在其中 1 个,那么:
interface IPerson {
boyName?: string,
grilName?: string
}
const person: IPerson = {
boyName: 'puxiao',
grilName: 'meinv'
}
---------- 或 ----------
type PersonType = {
boyName: string
} | {
grilName: string
}
const person: PersonType = {
boyName: 'puxiao',
grilName: 'meinv'
}
以上代码中,无论是 interface的 ?: 或 type的 |,都不能解决需求,该如何写?
经过微信群求助,最终网名叫 “夏笙” 的网友给出了比较符合的答案——使用泛型约束:
interface IPerson {
age: number
}
interface IBoyPerson extends IPerson {
boyName: string
}
interface IGrilPerson extends IPerson {
grilName: string
}
type PersonType<T> = T
// 这就是 泛型约束,要求目标类型和最终实例类型必须一致
const person: PersonType<IBoyPerson> = {
age: 34,
boyName: 'puxiao',
grilName: 'meinv'
// TypeScript错误提示:不能将类型“{ age: number; boyName: string; grilName: string; }”分配给类型“IBoyPerson”。
// 对象文字可以只指定已知属性,并且“grilName”不在类型“IBoyPerson”中。
}
假设某个类型的某属性也是复杂对象(复杂类型),例如:
interface IPerson {
info: {
name: string,
age: number
}
}
上述代码中,可以将 info 单独拿出来定义一个类型,然后在 IPerson 中使用,代码如下:
interface IInfo {
name:string,
age:number
}
interface IPerson {
info:IInfo
}
注意,后面的写法虽然也完全可以使用,但是 info 的语法提示略微差一些。 上面的写法会直接显示出完整的类型结构,但是下面的写法只会显示 IPerson.info: IInfo 个人建议是:如果不是属性数量过多、类型过于复杂,到了必须把属性分开写的程度,建议还是采用上面那种方式来定义类型。
直接看下面代码:
interface IURL {
url: string,
str: string
}
interface IAPI {
'/user': { name: string },
'/menu': { list: IURL[] }
}
const getData = async <URL extends keyof IAPI>(url:URL):Promise<IAPI[URL]> => {
return fetch(url).then(res => res.json())
}
getData('/user').then(res => res.name)
getData('/menu').then(res => res.list)
查找类型:IAPI['/menu'].list 的 IURL[] 泛型:<URL extends keyof IAPI>(url:URL):Promise<IAPI[URL]> keyof:URL extends keyof IAPI
原文举得例子,我没看明白,只能凭感觉,举一个自己写的例子,同时,对 keyof 的用法再次做一个回顾:
interface IPerson {
name: string,
age: number
}
const person: IPerson = {
name: 'puxiao',
age: 34
}
const changePerson: <K extends keyof IPerson>(name:K,value:IPerson[K]) => void = (key,value) => {
person[key] = value
}
changePerson('age',18)
changePerson('name','yang')
我认为原文中说的 显示泛型 相对的是 “隐式泛型”,所谓 “隐式”即从推理得到的泛型而言(用 typeof 获得的类型) <K extends keyof IPerson> === < T = typeof Xxx, K in T>
原文中作者自定义了一个类型,名为 DeepReadonly,他写的代码为:
type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>;
}
const a = { foo: { bar: 22 } }
const b = a as DeepReadonly<typeof a>
b.foo.bar = 33 // 嘿,出错了!
目前 TypeScript 官方自带的 Readonly ,经过测试,确实只能做到第1层的只读控制,无法做到深层的只读控制
interface IPerson {
name: string,
age: number,
info: {
work: string
}
}
const me: IPerson = {
name: 'puxiao',
age: 34,
info: {
work: 'development'
}
}
const you = me as Readonly<IPerson>
you.info = { work: 'coder' } //报错:无法分配到 "name" ,因为它是只读属性。
you.info.work = 'coder' //尝试修改第2层级的属性值,竟然没问题,不报错
把上面代码按照作者的方式,修改之后:
interface IPerson {
name: string,
age: number,
info: {
work: string
}
}
const me: IPerson = {
name: 'puxiao',
age: 34,
info: {
work: 'development'
}
}
type DeepReadonly<T> = {
readonly [P in keyof T]: DeepReadonly<T[P]>
}
const you = me as DeepReadonly<IPerson>
you.info = { work: 'coder' } //报错:无法分配到 "name" ,因为它是只读属性。
you.info.work = 'coder' //报错:无法分配到 "work" ,因为它是只读属性。
再次使用到了 泛型约束,只不过这次的形式非常特别,使用到了嵌套。
依照这个套路,还可以自定义出 DeepPartial(任何层级属性都变为可选)、DeepRequired(任何层级属性都变为必填)、DeepRecord(任何层级属性都变为指定类型)
在最新版 TypeScript 中,第一层属性变为只读,还有另外一种写法,使用 const 关键词:
const me = {
name: 'puxiao',
age: 34,
info: {
work: 'development'
}
}
const you = <const>{ ...me }
you.info = { work: 'coder' } //报错:无法分配到 "name" ,因为它是只读属性。
you.info.work = 'coder' //尝试修改第2层级的属性值,竟然没问题,不报错
在原文中,Omit 是作者自定义的泛型约束,代码如下:
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
在最新的 TypeScript 中,官方已经默认定义有 Omit,官方定义的形式为:
type Omit<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; }
虽然 2 者定义的形式略微不同,但是最终效果是相同的
高阶组件 或 工厂类中 比较合适使用 Omit。假设我们有一个需求:创建一个 Person 工厂函数,Person 有 2个 属性 name 和 age,但是传递给工厂函数的参数中不需要 age 对应的值(age的值由工厂函数内部产生获得)。
interface IPerson {
name: string,
age: number
}
type PropsType = Omit<IPerson, 'age'> //使用 TypeScript 自带的 Omit 泛型来获得 工厂函数需要的参数类型
const createPerson: (props: PropsType) => IPerson = (props) => {
return { ...props, 'age': Math.floor(Math.random() * 30) }
// Person 需要的 age 属性值由工厂函数内部随机一个数字
}
createPerson({ name: 'coder' })
Record 是 TypeScript 内置的映射类型之一,将选定属性名(或者由枚举而产生的属性名)对应的值类型全部转化为指定类型,官方定义的代码为:
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
Record 需要 2 个参数,第1个参数为约定属性名集合(由 enum 或 type 定义的枚举类型),第2个参数为约定属性值的类型。例如下面这段代码:
enum MoodType {
HAPPY = 'happy',
ANGRE = 'angre'
}
interface IData {
icon: string
}
const moodData: Record<MoodType, IData> = {
happy: { icon: 'aa' },
angre: { icon: 'bb' }
}
原文中没有细讲使用的好处细节是什么,这里只是贴一下原文中给的示例代码:
const mergeOptions = (options: Opt, patch: Partial<Opt>) {
return { ...options, ...patch };
}
class MyComponent extends React.PureComponent<Props> {
defaultProps: Partial<Props> = {};
}
我认为 Partial 就是将某些属性设置为可选,并没有明白上面示例中,展示使用技巧的点在哪里。
在 .tsx 文件中,泛型和tsx标签都采用 <> 形式,为了让泛型不被误解成标签,可以在泛型中加入 extends 来解决。
const toArray = <T>(element: T) => [element]; // 会被报错
const toArray = <T extends {}>(element: T) => [element]; // 不报错
在上面第一行代码中,VSCode 认为 <T> 是一个组件标签,会提示找不到对应的闭合标签 </T> 。 而下面那行代码中,加入了 extends 后,VSCode 即可正确识别出 该标签是 TypeScript 的泛型,而不是 tsx 的组件标签。
先补充一个 JavaScript 知识,以下文字来源于 MDN 中对 JS中 类、 class 的描述:
ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。这些新的关键字包括 class, constructor,static,extends 和 super。
对于使用过基于类的语言 (如 Java 或 C++) 的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并且本身不提供一个
class实现。(在 ES2015/ES6 中引入了class关键字,但那只是语法糖,JavaScript 仍然是基于原型的)。
几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。
划重点:JavaScript 中并不存在真正 面对对象 中的 类,JavaScript 是动态语言,走的是原型链。所以,当你看到下面这行代码:
class Person { constructor() { } }
你不要觉得你定义了一个名为 Person 的类,你只是借用 class 语法糖(对于原型链方便的操作)) 创建了一个Function实例。 同理,所谓实例化 Person,代码是 new Person() 又怎么理解呢? 再次划重点:new 这个关键词也是 JavaScript 中的一个语法糖,所谓 new Person() 真正执行的是 new.target ,其中 target 是 JavaScript 提供给我们方便在原型链上找到构造函数(constructor)的一种内部方式。
但是日常中,我们为了方便描述,依然会选择使用 类 或 实例化类 这样的词语。
在使用 高阶组件 或 工厂类 中,我们有时需要传入类本身,而非类的实例。在原文中,作者举了这样一个例子:
import React from 'react'
abstract class Animal extends React.PureComponent {
/* Common methods here. */
}
class Cat extends Animal { }
class Dog extends Animal { }
const renderAnimal = (AnimalComponent: Animal) => {
/* 报错:“AnimalComponent”表示值,但在此处用作类型。是否指“类型 AnimalComponent”? */
return <AnimalComponent/>;
}
renderAnimal(Cat); // 报错:类型“typeof Cat”的参数不能赋给类型“Animal”的参数。
renderAnimal(Dog); // 报错:类型“typeof Dog”的参数不能赋给类型“Animal”的参数。
通过自定义 ClassOf 泛型约束,可解决上述问题:
interface ClassOf<T> {
new (...args: any[]): T;
}
const renderAnimal = (AnimalComponent: ClassOf<Animal>) => {
return <AnimalComponent/>; //不再报错
}
以上是原文中举例代码,示例代码中所谓的 类 其实是 高阶组件(纯组件),而我平时更多使用 React 的函数组件,不怎么使用 类组件,我接触更多的是 函数 和 由 class 定义的类,所以我重新举了另外一个例子:
class Person { constructor() { } }
class Boy extends Person { }
class Gril extends Person { }
const renderPerson1 = (ClassName: Person) => {
return new ClassName() //报错:此表达式不可构造。类型 "Person" 没有构造签名。
/* 例如:let num:number,那么是无法 new num() 的。
* 参数 ClassName 只不过是 Person 的一个实例,所以无法 new ClassName()
* 并不是说 Person 是个实例
*/
}
const renderPerson2 = (ClassName: typeof Person) => {
return new ClassName()
}
interface ClassOf<T> {
new (...args: any[]): T;
}
const renderPerson3 = (ClassName: ClassOf<Person>) => {
return new ClassName()
}
let boy = renderPerson1(Boy) //不报错
let gril = renderPerson2(Gril) //不报错
let person =renderPerson3(Person) //不报错
很明显,通过上述代码可以知道,如果是 class 定义的类,最简单的办法就是在 传入该类的时候,采用:ClassName: typeof Person 即可。当然使用 ClassOf 也是可以的。
但是,我思考的是如果是工厂类,那为什么不改成这种写法,更加简单直白:
const renderPerson1 = (ClassName = Person) => {
return new ClassName() //不报错
}
原文中举的例子是 类组件与之组件 函数 传递下去,为了通过以下方式,在子组件中定义:
class Parent extends React.PureComponent {
private updateHeader = (title: string, subTitle: string) => {
// Do it.
};
render() {
return <Child updateHeader={ this.updateHeader } />;
}
}
//默认子组件中应该这样定义
interface ChildProps {
updateHeader: (title: string, subTitle: string) => void;
}
//使用类型查找,修改成这种方式后,可以省掉很多重复的代码(参数、返回值等)
interface ChildProps {
updateHeader: Parent['updateHeader'];
}
class Child extends React.PureComponent<ChildProps> {
private onClick = () => {
this.props.updateHeader('Hello', 'Typescript');
};
render() {
return <button onClick={ this.onClick }> Go < /button>;
}
}
由于我不用类组件,我用函数组件,我暂时不太能理解这样做的好处。
Pick<T,U>:取一部分
Partial<T>:全部变为可选属性 ?:
Required<Type>:全部变为必填属性
Readonly<T>:全部变为只读属性
Record<Keys,Type>:将选定属性名(或者由枚举而产生的属性名)对应的值类型全部转化为指定类型
Omit<Type, Keys>:删除指定属性
Exclude<T,U>:排除相同的,剩下所有不相同的
Extract<T,K>:前后两者中共同拥有的
NonNullable<T>:排除所有 null 或 undefined,值保留可用的
其他获取——映射类型:ReturnType、Parameters、ConstructorParameters、InstanceType、ThisParameterType、OmitThisParameter、ThisType
ReturnType<T>:定义函数返回值类型
Parameters<Type>:获取所有参数类型
ConstructorParameters<Type>:获取构造函数所有参数类型
InstanceType<T>:获取类返回对象的类型
ThisParameterType<Type>
OmitThisParameter<Type>:
ThisType<Type>:
默认元祖数组只能约束元素类型,但是无法约束数组长度,可通过以下定义来实现初次赋值时进行长度限定。
type Tuple<T, N extends number> = [T, ...T[]] & { length: N }
type MyArr = Tuple<number, 7>
const arr:MyArr = [0,1,2,3,4,5,6]
特别提醒:上述中的 Tuple 仅仅只是约束第一次初始化赋值时数组的长度,但是 实例 arr 依然可以执行后续的 push、pop 等操作,来改变数组的长度。
先通过枚举定义若干常量,然后根据枚举对象的属性名(键名),重新得到一个指定属性值类型的约束对象。
这里面使用了:K in keyof typeof E 这种组合
//错误状态码
enum LoginFailCode {
unknowCode = 10,
authorizationCode = 11,
loginDbCode = 12
}
//以下要定义错误状态码对应的错误提示信息
//第1种定义方法
type EnumType<T> = { [key in keyof typeof LoginFailCode]: T };
const LoginFailMsg1: EnumType<string> = {
unknowCode: '用户登录时,发生未知错误',
authorizationCode: '用户登录时,获取openid发生错误',
loginDbCode: '用户登录时,数据库操作发生错误'
}
//第2种定义方法
const LoginFailMsg2: Record<keyof typeof LoginFailCode, string> = {
unknowCode: '用户登录时,发生未知错误',
authorizationCode: '用户登录时,获取openid发生错误',
loginDbCode: '用户登录时,数据库操作发生错误'
}
在上面小节中,可以通过 keyof typeof 获取 枚举对象的所有键名,并将键名组成联合类型,但是目前来说是没有办法将枚举对象键值组成联合类型。
目前来说,可以通过不使用 enum,改成 const 来实现。
const MyCount = {
A: 1,
B: 5,
C: 8
} as const
//获取 MyCount 所有键名 组成的联合类型,即:"A" | "B" | "C"
type Keys = keyof typeof MyCount
//获取 MyCount 所有键值 组成的联合类型,即:1 | 5 | 8
type Values = typeof MyCount[keyof typeof MyCount]
通常情况下定义枚举对象的方式为:enum Xxx {},Xxx 编译后是一个对象。如果在定义时添加 const,即:const enum Xxx{},那么这样定义的枚举对象只有在编写代码,编译过程中存在,编译后则会消失。
enum EnumA { A = 1, B = 5, C = 8 }
const enum EnumB { A = 1, B = 5, C = 8}
const aa = EnumA.A
const bb = EnumB.A
// EnumA 和 EnumB 的区别是什么 ?
// EnumA 被编译后是一个对象,而 EnumbB 编译后则会消失
// aa 和 bb 被编译后的区别是什么 ?
// aa = EnumA.A、bb = 1
/**
EnumA 被编译后是一个对象,长这个样子:
var EnumA;
(function (EnumA) {
EnumA[EnumA["A"] = 1] = "A";
EnumA[EnumA["B"] = 5] = "B";
EnumA[EnumA["C"] = 8] = "C";
})(EnumA || (EnumA = {}));
而经过 TS 编译后的代码中,根本不存在 EnumB
*/
使用 const 定义的变量,虽然对象本身类型不能再发生变化,但是该对象的属性却可以被修改。
通过以下 2 种方式,均可以让 对象 属性也变为只读(哪怕属性本身也是一个复杂类型的对象)
const person = { name:'puxiao', age:'34'}
person.age = 18 //可以被修改
const person = <const>{ name:'puxiao', age:'34'}
或
const person = { name:'puxiao', age:'34'} as const
person.age = 18 //无法分配到 "age" ,因为它是只读属性。
在最新的 TS 4 版本中,tsconfig.json 中 strict 默认值为 true,即 默认开启严格模式。
在严格模式下 无论 noImplicitAny 的值是 true 还是 false,如果代码中 TS 自动推断出有值为 any,就会报错:
元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "{}"。
在之前默认非严格模式下,以下代码是没有问题的:
const removeUndefined = (obj: object) => {
for (let key in obj) {
if (obj[key] === undefined) {
delete obj[key]
}
}
return obj
}
但是如果是 TS 4 的严格模式下,只有修改成以下代码后才可以:
const removeUndefined = (obj: object) => {
for (let key in obj) {
if (obj[key as keyof typeof obj] === undefined) {
delete obj[key as keyof typeof obj]
}
}
return obj
}
const arr = ['a','b','c'] as const
type value = typeof arr[num] // 'a'|'b'|'c'
假设 TypeScript 开启的是严格模式,若有已定义但从未读取使用的变量就会收到 TS 报错。
例如下面这段代码:
arr.forEach((value,index) => {
...
index
...
})
在上面这段代码中,我们利用了数组的 forEach() 来循环遍历,但是我们真正执行的代码中只用到了 index,并没有用到 value,那么 TS 就会报错:
错误:已定义 value,但从未读取该值
这种情况下,我们可以改用 for 循环,例如:
for(let i=0; i<arr.lenght; i++) {
...
index
...
}
这样做肯定没有问题,但是假设我就是想用 forEach() 函数,就是不用 value,又希望 TS 不报错该怎么办呢?
解决办法:可以使用 下划线 _ 这个特殊符号来作为变量名,可起到占位作用。
将原本的 forEach() 代码修改如下:
arr.forEach((_,index) => {
...
index
...
})
此时 下划线 就起到了一个 “占位符” 的作用,TS 不会针对 下划线 _ 进行 “已定义从未读取” 的检测,不再报错。
补充说明:
- 使用下划线来作为变量名,这其实是 JS 所支持的
- TypeScript 只是不针对 下划线 变量 进行 已定义但从未读取的检测
延伸补充:
下划线 _ 可以作为 JS 支持的变量名,我们常见的定义箭头函数 () => { ... } 可以简化为 _=>{ ... }
这里面 _ 充当一个无用的参数。
当然这种写法并不特别提倡,毕竟代码阅读性不是特别好。
如果别人不知道 下划线 _ 可以来充当变量名,起到占位作用,是会看懵的。
在日常 TS 使用中,我们会使用这种形式
type xxx = string | number | any[]
这种形式叫 “联合类型”,和这个形式类似的还有另外一种高级用法——可辨识联合类型
可辨识类型的使用示例:
interface Aaa {
type: 'a',
name: string
}
interface Bbb {
type: 'b',
age: number
}
interface Ccc {
type: 'c',
list:string[]
}
type ABC = Aaa | Bbb | Ccc
我们先定义彼此不相干的 3 个类,最后通过 Aaa | Bbb | Ccc 这种类似联合的形式组成了 ABC。
那么 TypeScript 会去检查 3 个类的共同特点,然后推理出 ABC 应该拥有的特点。
- TS 发现这 3 个类的共同特征是都拥有 type 属性,且 type 属性的类型都不相同。
- 当我们在其他地方使用 ABC 类型时,就可以通过 ABC.type 来判断出究竟是哪个类的实例,并且给出该类型特有的其他属性语法提示和检查。
换一种说法:
在传统的面向对象编程语言中,我们必须先定义好父类,才能再定义子类。但是在 TS 中,我们可以先定义若干个 “子类”,然后将这些 “子类” 联合起来,让 TS 推理出 他们的 “父类” 应该是什么样子。
切记,这些单独定义的 “子类” 应该至少有 1 项有相同的属性名且属性类型不同,这样的 ABC 才可以备 TS 可辨识推理出来。
请注意是相同的属性名、不同的属性类型
如果是相同的属性名、不同的属性值,TS 是无法推理的。
应用场景:定义多个具有相似结构的子类,然后通过 Xxx.type 进行 TS 实例推理,得到对应具体子类的属性语法提示和检查。
关于 类 class 在 TS 中的知识点补充:
假设我们定义一个 Person 的类
export class Person {
name: string
constructor() {
this.name = 'ypx'
}
}
上面代码中 Person 包含 2 层意思:
-
一个名为 Person 的类
-
一个名为 Person 的类型,相当于
interface Person { name: string }或者是
type Person = { name: string }
因此我们即可以把 Person 当类使用,也可以把 Person 当接口(interface) 或 类型别名(type) 来使用。
还有 2 个点需要了解一下:接口合并、接口实现
接口合并:
export interface IPerson {
name: string
}
export interface IPerson {
age: number
}
接口实现:
export interface IPerson {
name: string
}
export interface IPerson {
age: number
}
//类型“Person”缺少类型“IPerson”中的以下属性: name, age
export class Person implements IPerson {
constructor() {
}
}
上面代码中,Person 需要实现 IPerson 所规定的 2 个属性,由于没有还未实现所以 TS 会报错。
接口实现的错误演示示例:
interface Person {
name: string
}
interface Person {
age: number
}
//下面为错误的 接口实现 方式
class Person {
constructor() {
}
}
//或
class Person implements Person {
constructor() {
}
}
请注意,上面代码在 TS 中并不会报错,恰恰是因为没有报错才证明我们接口没有实现。
这是因为我们定义的 class 名字 Person 和 接口名字 Person 相同,那么 TS 其实把 class Person { ... } 重新定义出一个 Person 的类型。
也就是说错误示例中其实发生的并不是接口实现,而是 3 个 Person 接口合并。