项目开发中,我们会遇到这样的需求。就是我们需要开发一个函数或者一个插件或者库等等,需要调用者调用时传入指定的参数,比如我们需要的是string类型的或者对象类型的,再或者是对象类型的且里面的属性必须包含什么属性等,如果说我们自己的约定属于彼此之间的约定,那么接口就是ts为你的代码定义的强制契约。
一般情况下的我们定义的约定
const printFullName = ({ firstName, lastNmae }) => return `${ firstName } ${ lastName }` printFullName({ firstName: '爱新觉罗', lastName: '小胖纸' }) // 输出 爱新觉罗 小胖纸
我们期望的是这样,但是呢?一句话怎么说来着,你永远不要去揣测用户的心里。
可能他们调用的时候传的是这个
printFullName({ firstName: '爱新觉罗', lastName: 18 })
这样传参是合法的,但是却不是我们想要的,而结果当然也是不可预期的。
接口初探
那么接下来我们看接口是如何进行约束的
ts规定了使用interface声明一个接口
interface FullName { firstName: string lastName: string }
// 接着我们改造下上面的函数定义 const printFullName = ({ firstName, lastNmae }: FullName ): string => return `${ firstName } ${ lastName }`
这样我们就给函数的参数进行了一层约束,约束规则如下
必须是一个对象 对象必须包含firstName和lastName属性 firstName和lastName的属性值必须是string类型 // 我们试着调用下 printFullName({ firstName: '爱新觉罗', lastName: '小胖纸' }) // success 爱新觉罗 小胖纸
printFullName({ firstName: '爱新觉罗', lastName: 18 }) // error lastname不能将number类型分配给string
这样我们就约束了调用者的传参,其结果也就符合预期了
可选属性
还是拿上面的例子举例,可选属性就是在对象的后面添加?
// 定义一个接口,firstName 必须有,lastName和age可有可无 interface FullName { firstName: string lastName?: string age?: number }
const printFullName = ({ firstName, lastName = '', age }: FullName): string => { return `${ firstName } ${ lastName } ${ age }` } printFullName({ firstName: '爱新觉罗' } // 爱新觉罗 printFullName({ firstName: '爱新觉罗', age: 22 } // 爱新觉罗 22 printFullName({ firstName: '爱新觉罗', lastName: '小胖纸', age: 22 } // 爱新觉罗 小胖纸 22
这样我们在传参时,就可以根据条件来进行对应传参
只读属性
顾名思义,只读属性规定了对象只能在初始化时对其进行赋值,一旦初始化完毕,就修改不了
// 定义一个接口,firstName interface FullName { firstName: string readonly lastName: string }
const fullName: FullName = { firstName: '爱新觉罗', lastName: '小胖纸' } fullName.lastName // 小胖纸 // 当我们尝试着修改时 fullName.lastName = '大胖纸' // error lastName is read-only
同样的数组也是对象的一种,在重温下数组的定义
const arr: number[] = [1, 2, 3] // 或 const arr: Array<number> = [1, 2, 3]
因此对于数组而言也有只读属性
const arr: ReadonlyArray<number> = [1, 2, 3] arr[1] // 2 // 当我们尝试着去修改值时 arr[1] = 1 // error arr is read-only
当然你可以使用类型断言重写arr,但是既然设置了只读,就不应该再去修改它,这里只做说明,不推荐
额外的属性检查
对于刚刚我们定义的对象不知道有没有小伙伴觉得疑惑,为什么,接口定义了几个属性,参数就传递了几个或者创建的对象就只有那么几个属性。这里集中说明下
// 这里我们定义一个接口 interface FullName { firstName: string lastName: string } // 当我们使用接口时 const fullName: FullName = { firstName: '爱新觉罗', lastName: '小胖纸', age: 22 } // error 因为FullName没有包含age属性
尴尬了,难道就只能传这两个嘛,有时候赋值一个对象,里面可多属性的怎么破
使用类型断言
我们把赋值的对象断言成符合要求的FullName接口类型
const fullName: FullName = { firstName: '爱新觉罗', lastName: '小胖纸', age: 22 } as FullName
这样赋值的类型就满足要求了
使用类型兼容
举个例子,假设我有500平房子,500万车子,1000万存款(开玩笑哈),你想和我对等换,那么你是不是也得有房子,车子和存款是吧,当然你要是多加点股票啥的我更乐意,这就说明了类型兼容,你要赋值给我,那么你的属性除了和我一样之外,还必须比我多,那么类型兼容就是这样
让我们使用类型兼容试试
// 这个就是你有的 而接口就是我有的,你比我的多 const fullNameAll = { firstName: '爱新觉罗', lastName: '小胖纸', age: 22 } const fullName: FullName = fullNameAll
这样就行了,绕过了接口的额外属性检查
使用索引签名
索引签名是个啥,先理解索引,对象的索引是key,数组的索引是下标,那么索引签名呢?就是可以通过索引获取到值,让我们用例子看,还是刚才的例子
// 这里我们定义一个接口 interface FullName { firstName: string lastName: string [ key: string ]: number }
这里我们为interface接口加了索引签名,意思是什么呢,就是我们通过string类型去索引FullName,可以获取到number类型的返回值,这样的想法是正确的,但运行之后我们会发现,竟然报错了,原来索引签名的返回值类型必须是已经定义的父类型。翻译过来的大白话就是,[ key: string ]: number这个东西就是个可变类型,可以匹配任意多个属性是age: 22这样的属性,而我们已经定义好的属性确是firstName: string返回值类型不匹配,所以报错,那么如何解决呢?
第一就是保持类型统一,这个限制很大,不可能刚刚好都是一个类型。
第二就是把索引签名的返回值类型设为any。这样就可以了
interface FullName { firstName: string lastName: string [ key: string ]: string } // 或 interface FullName { firstName: string lastName: string [ key: string ]: any }
当然这是对象的索引签名,还有数字的索引签名,碧如
interface StrArr { [ index: number ]: string } const strArr: StrArr = ['小胖纸', '大胖纸']
还有一个需要主要的点就是使用数字索引时,会先转换成string类型再去索引
这里我们看到string类型覆盖掉了number类型的索引
当然,索引签名也是可以使用只读属性的,用法和上面一样
接口的继承
接口是可以继承的,想类那样使用extends关键字就可以
interface FullName { firstName: string lastName: string } interface FullInfo extends FullName { age: number } const fullInfo: FullInfo = { firstName: '爱新觉罗', lastName: '小胖纸', age: 22 }
缺少任意一项就会出现额外类型检查的错误
接口对函数的约束
函数式js的一等公民,它也是对象,因此,接口对函数也是起作用的,让我们来看个例子
interface PrintFullName { (firstName: string, lastName: string): string } const printFullName: PrintFullName = (firstName, lastName): string => `${ firstName } ${ lastName }` printFullName('爱新觉罗', '小胖纸') // 爱新觉罗 小胖纸
这里我们使用(firstName: string, lastName: string) => string来对函数的传入参数和返回值类型进行约束,传入参数是string类型,返回值也是string类型
另外需要注意的是,函数类型的接口,只检查对应参数的值类型,而不需要值相等
const printFullName: PrintFullName = (str1, str2): string => `${ firstName } ${ lastName }`
你也可以给参数指定类型和函数的返回值不指定类型
const printFullName: PrintFullName = (str1: string, str2: string) => `${ firstName } ${ lastName }`
一般而言,函数类型在定义函数时我们是不需要指定返回值类型的,ts解析器能够推断出来
还有对于函数类型的接口约束推荐使用type类型别名来定义,上面的列子我们在改写下
type PrintFullName = (firstName: string, lastName: string) => string
接口的混和类型
对于对象而言,我们可以定义属性,那么我们是不是也可以定义方法,答案当然是阔以了。比如
// 定义一个对象,里面包含属性和方法 interface PrintInfo { userName: string (age: number): void print(): void } const printInfo = (): PrintInfo => { const p = (age: number) => { console.log(`${ age }`) } p.userName= '小胖纸' // 主要这里不能是name 设为name则是想改掉函数本身自带的name属性 p.print = () => {} return p } let p = printInfo() p(22) // 22 p.userName // 小胖纸 p.print() // // 或者这么写 const printInfo = (): PrintInfo => { let p = <PrintInfo >((age: number) => { console.log(`${ age }`) }) p.userName= '小胖纸' // 主要这里不能是name 设为name则是想改掉函数本身自带的name属性 p.print = () => {} return p }
类可以实现接口
如果你希望在类中使用必须要被遵循的接口(类)或别人定义的对象结构,可以使用 implements 关键字来确保其兼容性
interface FullName { firstName: string lastName: string } class Name implements FullName { firstName: string lastName: string }