结构检查
再讲接口前,首先要理解,TS经常会对数据结构进行检测
摘抄官网的一句话 :TypeScript的核心原则之一是对值所具有的结构进行类型检查
通过代码来理解这句话。
function man(person: {name: string, age: number}) {
console.log(person.name, person.age)
}
复制代码
上面这个man
函数的参数person
只能接受带有name: string
和 age: number
的对象。这就是所谓的结构
function man(person: {name: string, age: number}) {
console.log(person.name, person.age)
}
man({
name: 'lisa',
age:18 // ok
})
复制代码
如果传入的值不符合{name: string, age: number}
这种结构且类型也不正确,就会报错。
传入的值必须符合要求。这就是所谓的对于值的结构进行类型检查
分为结构检查
和类型检查
下面要讲的接口
使用时的报错情况就是基于此。
接口
接口
就是用来描述数据结构的。也可以理解为一种代码规范,有约束代码的作用
例如,将上面代码的写到接口里。
使用 interface
关键字 来定义接口
interface Iperson {
name: string,
age: number
}
function man(person: Iperson) {
console.log(person.name, person.age)
}
//等价
function man(person: {name: string, age: number}) {
console.log(person.name, person.age)
}
复制代码
Iperson
是接口
的名字,接口
就像是我们自定义的类型,使用起来和类型一样。TS也会检测我们的代码符不符合接口
里的规范
interface Iperson {
name: string,
age: number
}
function man(person: Iperson) {
console.log(person.name, person.age)
}
man({
name: 'lisa',
age:18 // ok
})
man({
name: 'lisa',
age:'men' //error 类型不兼容
})
复制代码
接口
里的代码不能提供具体的实现。只能定义结构类型(xx值是xx类型 这是ok的)
函数接口
除了能够描述普通的带属性的对象外,接口
还可以描述函数的类型。
在接口
里定义函数的参数列表和返回值类型。每个参数都需要名字和类型。
interface Person_2 {
//定义函数的参数类型和返回值类型
(name: string, age:number):void
}
复制代码
定义好后我们可以使用这个接口
创建一个函数,函数的参数名不需要与接口里定义的名字相匹配:
let func1: Person_2 = (sex:string, num:number):void => {} //ok 参数类型匹配即可,名字无所谓
let func2: Person_2 = (sex:string, num:string, dd:string):void => {} //error 多写了一个参数。结构不匹配
复制代码
下面实现一个复杂点的函数类型接口
interface Actual_1 {
name: string
age: number
}
interface Person_3 {
//使用ES6的解构赋值,获取参数
({name, age}: Actual_1): void //。这里 Actual_1接口主要约束的是实参。实参只能带有name和age属性
}
//接下来,按照给定的接口实现这个函数
//正确版本:
//这个函数的形参在接口中定义 是通过ES6对象的解构赋值获取的,如果要改参数名字,需要使用对象解构赋值的语法。
let func: Person_3 = ({name: Iname, age}: Actual_1): void => { // == {name: Iname, age} = {name:xxx, age:xxx}
console.log(`${Iname}, ${age}`) // ok
}
func({
age: 18,
name :'boolean'
})
//错误版本:
let func: Person_3 = ({name, iage}: Actual_1): void => { //错误 Actual_1没有定义iage这个属性。
console.log(`${name}, ${iage}`)
}
func({
age: 18,
name :'boolean'
})
-------------------------------------------------------------------------------------------------------------
//上面的函数不用接口实现的写法:
let func= ({name, age}:{name: string, age: number}): void => {
console.log(`${name}, ${age}`)
}
func({
age: 18,
name :'boolean'
}) //ok
//接口的好处在于复用,可扩展性。
//上面的Person_3接口,可以用来约束很多相同的函数。或者直接使用Actual_1这个接口来约束对象。
复制代码
可选属性
可选属性的意义在于,不使用没关系,使用的话约束其类型正确
借此体验一下接口
的可扩展性,我们把上面的代码copy过来
interface Actual_1 {
name: string
age: number
}
//想想怎么再此基础上扩展一个sex属性使用。
interface Actual_1 {
name: string
age: number
// sex: string // 这样不行,因为上面很多代码实现了这个接口。直接添加这个属性,会导致其他代码必须实现这个属性。从而引发一大堆错误
sex?: string // ok 可选属性. 在属性名后面,冒号前面添加一个问号(?),则表明该属性是可选的。可有可无的属性。
}
let obj1: Actual_1 = {
name: 'dd',
age: 90,
sex: 'man' //ok
}
let obj2: Actual_1 = {
name: 'dd',
age: 90 //ok 可选属性就是可有可无的
}
let obj3: Actual_1 = {
name: 'dd',
age: 90,
sex: 123 //error 使用的话必须符合规范
}
复制代码
接口继承
也可以使用接口继承
,扩展上面的例子
使用 extends
关键字:接口 extends 接口
interface Actual_1 {
name: string
age: number
}
interface superActual extends Actual_1 {
sex: string
//这里面是这个样子
// name: string
// age: number
// sex: string
}
//现在这个superActual上就有了三个属性
//在将其实现
let obj: superActual = {
name: 'dd',
age: 90,
sex: 'man' //ok
}
复制代码
一个接口也可以同时继承多个接口,实现多个接口成员的合并。用逗号隔开要继承的接口。
接口 extends 接口1,接口2.....接口n
需要注意的是,尽管TS支持继承接口,但是如果继承的接口中,定义了同名属性但类型不同的话,是不能编译通过的
只读属性
只读属性
: 只可以读取,不可以修改的属性
定义只读属性
:要在属性前面加上 readonly
关键字
interface Immutable {
prop: string
readonly x: number
readonly y: number
}
let num: Immutable = {
x: 100, //赋完值后就不可修改了
y: 99,
prop: 'xxx'
}
num.prop = 'aaa' //ok
num.x = 1 //error 只能读取,不可修改
num.y = 2 //error
复制代码
索引签名
索引签名
官网定义:可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型
解释一下:索引
也是有类型的,比如a[10] 和 a['10']
。分别为number
类型索引和string
类型索引,然后通过这些索引会返回什么类型的值
语法:
[prop: string] // prop 和index是随便起的名字, a b c d 都行
[index: number]
//索引签名只能为 number 或者 string,二者其中一个
复制代码
让我们来定义一个带有字符串索引的接口
interface oop {
readonly [prop: string]: any //通过字符串索引返回任意类型的值
}
//这接口实现看起来很像普通对象... 因为对象的索引原本就是字符串形式
let obj: oop = {
a: 1,
b: 2,
c: 'e'
}
//现在obj里面的属性都是只读类型
obj.a //1
obj['b'] //2
obj.c = 2 // error 不可修改。
复制代码
在定义一个带有数字索引的接口
interface numberArray {
[index: number]: number
//key是数字类型 : value是数字类型
}
let numArr1: numberArray = [1,2,3,55,6,'1'] // error 不能将字符串赋值给number类型
let numArr1: numberArray = [1,2,3,55,6] //类似于 number类型数组
let numArr2: number[] = [1,2,3,45,6,6] //number类型数组
// 两者看起来也差不多,也都能取值和修改
numArr1[1] // 1
numArr2[1] // 1
numArr1[1] = 77 //ok
numArr1[1] // 77
// 但无法使用数组的方法
numArr1.forEach() //error numberArray类型上不存在forEach方法。自定义的类型嘛,当然不存在啦
numArr2.forEach() // ok
复制代码
来定义一些复杂的数据,下面定义一组用户数据:
interface UserJSON {
[username: string]: {
id: number
age: number
sex: string
}
}
//将其实现。
let obj: UserJSON = {
Lisa: {
id: 1,
age: 18,
sex: 'man'
},
Bob: {
id: 2,
age: 19,
sex: 'man'
},
Sam: {
id: 3,
age: 20,
sex: 'man'
}
}
复制代码
索引签名的注意点:
1. 相同的索引签名不能重复
2. 当使定义了 string索引签名 时,其余所有成员都必须符合 string索引签名的规范
interface StrInter {
[key: string]: number // 这是个string类型的索引签名。它返回number类型的值。
a: string // error 报错 : 类型“string”的属性“a”不能赋给字符串索引类型“number”。
}
//假设上述接口能使用
let obj: StrInter = {
// 当实现这个接口时,规定了字符串索引只能接受number类型的值
// 属性 a 本质上也是作为一个字符串索引的存在 obj[a]~
// 所以接口中的a属性必须按照字符串索引的规范来写,指定为number类型或者any类型
c:12,
a: 'haha' // 小老弟咋回事??谁让你是接受字符串类型的值了
}
// 修改版:
interface StrInter {
[key: string]: number
a: number //ok
//a: any //ok
}
--------------------------------------------------------------------------------------------------------------------
//number类型的索引签名就没有这么多问题了
interface NumInter {
[key: number]: number //不会影响其他属性
a: string //ok
}
let arrObj: NumInter = {
1:1,
2:2,
a: 'true', //a属性也是原样赋值
3:3
} //ok
arrObj[1] // 1
arrObj['1'] // 1 因为对象取值会进行toString,把属性都转化为字符串
arrObj['1'] === arrObj[1] //true
arrObj[a] // 'true'
复制代码
3. 两种索引签名一起使用时,必须与string索引签名返回值类型一致(当然最好不要两个一起用,没意义...)
interface User1 {
[b: number]: number //error 返回值类型不兼容
[a: string] : string
}
// 上面的接口的索引类型的返回值,会造成冲突,因为JavaScript中数值索引会被转换成字符串索引(隐式类型转换)。
// 在JS中下列代码是正确的
const a = [1, 2, 3];
a[1] === a['1'] // true
----------------------------------------------------------------------------------------------------------------------------
//正确版:
interface User2 {
[b: number]: string //ok
[a: string]: string
}
let obj: User2 = {
1 : 'hah', // 涉及隐式类型转换
a : '2',
c : '3'
}
obj[1] // 'hah'
obj['1'] // 'hah'
//其实数字索引返回值类型 是字符串索引返回值类型的子类型也可以
interface User2 {
[b: number]: null / undefined / never / any //这些类型都可以,但没什么意义。
[a: string]: string
}
复制代码
函数
TS函数没什么特别的和普通JS一样。也支持ES6箭头函数,默认参数,...运算符等等
// function 声明
function add1(arg1: number, arg2: number): number {
return arg1 + arg2
}
// 箭头函数
let add2 = (arg1: number, arg2: number): number => arg1 + arg2
------------------------------------------------------------------------------------------------------------------
// 上面代码其实是简写,仔细观察add1和add2后面并没有,add1:xxx,add2:xxx。没有指定类型
// 而是直接赋值。这是因为TS的类型推断(将慢慢深入这个知识),会帮我们推断出来函数的结构
// 这是上面的实际写法(以add2为例)。先指定 函数的结构,在实现这个函数
let add2: (x: number, y: number) => number // 这里 => number 是返回值的类型,不是函数体
//实现函数,函数对于参数的名字不做限制哦
add2 = (arg1: number, arg2: number): number => arg1 + arg2
//注意下面没有明确指定 返回值类型。但TS会进行类型推断,推断出来是number。 因为 number + number 必定是返回number类型。
// 1 + 1 = 2 总不能 1 + 1 = '2' ????变成字符串吧~~~
add2 = (arg1: number, arg2: number) => arg1 + arg2
// 连在一起是这个样子 等号 左边函数结构,右边是具体实现的函数
let add2: (x: number, y: number) => number = (arg1: number, arg2: number): number => arg1 + arg2
//可以看出来,类型推断为我们省了很多代码
------------------------------------------------------------------------------------------------------------------
复制代码
类型别名
就是给某些类型,起个别的名字来代替
使用type
关键字来声明
就跟平时生活中为了方便直接叫别人小张,小李一样。而不叫全名
// 假设这段 (x: number, y: number) => number 函数类型需要用很多次。难不成每次都要写这么一大坨代码?
// 可以起个短点的名字
type AddFunction = (arg1: number, arg2: number) => number
// 用 AddFunction 来代替 (arg1: number, arg2: number) => number 这段代码
let myFunc2: AddFunction = (arg1: number, arg2: number) => arg1 + arg2
---------------------------------------------------------------------------------------------------------------------------
// 也可以使用上面的接口知识来封装起来。
interface MyFunc {
(x: number, y: number) :number
}
let myFunc1: MyFunc = (arg1: number, arg2: number) => arg1 + arg2
复制代码
类型别名看起来和接口很像,但没有接口那么灵活和可扩展性(继承)。
interface AddFunct {
(arg1: number, arg2: number) : number;
a: string;
}
type AddFunction = {
(arg1: number, arg2: number) : number;
a: string;
} //ok,但不推荐这样使用类型别名。
// 类型别名更适合存储那种很长的类型字面量,没有什么复杂的逻辑。
type Tarr = [string, number, boolean]
type Itype = string | number | null | boolean //这是联合类型语法,属于高级类型的知识。这里是使用为了代码演示
复制代码
可选参数
TS的可选参数
必须在 必选参数后面,否则会保错
语法与可选属性一样
type AddFun = (arg1?: number, arg2: number, arg3: number) => number //error 可选参数必须在必选参数后面
type AddFun = (arg1: number, arg2: number, arg3?: number) => number; //ok
let addFunc:AddFun
addFunc = (x: number, y: number) => x + y //ok 可选参数就是可有可无的
addFunc = (x: number, y: number, z: number) => x + y + z //ok
复制代码
默认参数
语法跟ES6函数默认参数一样
let addFun = (x:number, y:number, z:number = 3):number => x + y + z
addFun(1, 2) // 6
//这个函数还可以再简化点。让TS使用类型推断,来帮我们识别部分代码类型
let addFun = (x:number, y:number, z = 3) => x + y + z //TS会根据默认值 自动推断出 z 的类型是number
addFun(2, 2) // 7
addFun(2, 2, '2')//error 字符串不能赋予number类型
复制代码
剩余参数
也是ES6的知识...
const func = (...args: number[]) => console.log(args)
func(1,2,3,5,6) //ok [1,2,3,5,6]
func(1,2,'3',5,'6') //error 类型不匹配,字符串不能赋予number类型
//下面参数args的类型是any[],所以随便传值
const func1 = (arg1:number, ...args: any[]) => {
args.push(arg1)
console.log(args)
}
func1(1,2,3,5,6) //ok [2,3,5,6,1]
func1(1,2,'3',5,'6') //ok [ 2, '3', 5, '6', 1 ] any类型数组可以存所有类型的的值
复制代码
重载
TS的函数重载,跟c++,java等重载表现形式不一样。
c++,java中的重载,其实就是使用相同的函数名,传入不同数量的参数或不同类型的参数,使同名的函数,有不同的表现。
//摘抄自维基百科的代码:
public class Test{
public void A(){ //这是一个无形式参数名称为A的函数。
}
public void A(int a){ //这个函数有一个数据类型为int的函数,函数参数不同,故构成重载。
}
public void A(String a){ //这个函数数据类型为String,形式参数的数据类型不同,故构成重载。
}
public void A(int a,int b){ //这个函数有两个形式参数,故构成重载。
}
}
不用在意语法。能看出来,上面的函数A根据参数的变化。能干许多不同的事情。
--------------------------------------------------------------------------------------------------------------------
javascript没有函数重载这个概念。所以一般都是通过判断函数参数来模仿,实现类似的行为。
function func(x) {
if(typeof x === 'number') {
console.log(x)
} else if (typeof x === 'string') {
console.log(x.split(','))
} else {
// xxxxxx
}
}
func(2)
func('hello')
复制代码
而TS中的函数重载,更像是一份说明文档 + 类型检测。给开发者看,这个函数传入不同的参数有什么返回值.
//TS能实现重载的函数只能是使用 function 函数声明定义的的函数
//代码演示:
function handleData(x: number[]): string // 接收一个number 数组,返回字符串类型的值
function handleData(x: string, y: number):string // 如果传入两个参数就相加,返回字符串类型的值
function handleData(x: any, y?: any): any { // 处理上述的所有情况的函数
if(typeof x === 'number' && y != undefined) {
return x + y;
}
return x.toString();
}
handleData([1,2,3,4,5,6])
handleData('hello: ', 2019)
复制代码
TS的函数重载分为两部分:1.多次书写的函数声明 2.一个处理所有函数声明的函数
1. 同一个函数的多次函数声明...定义了不同的参数和返回值
function handleData(x: number[]): string
function handleData(x: string, y: number):string
2. 这个函数是所有函数声明(?)的具体实现。内部通过判断不同的参数,处理不同的函数声明
function handleData(x: any, y?:any): any {
if(typeof x === 'number' && y != undefined) {
return x + y;
}
return x.toString();
}
简单的来说就是,把每一种特定的函数声明写出来,最后实现一个能够处理所有参数类型的函数
//上述代码编译成JS代码
function handleData(x, y) {
if (typeof x === 'number' && y != undefined) {
return x + y;
}
return x.toString();
}
会发现只不过除去多次函数声明。其他与使用JS模仿实现重载一样。
--------------------------------------------------------------------------------------------------------------------
但TS重载并非鸡肋,看起来多写了几行无关紧要的代码。但能提供更加严格的代码检查
handleData('s') //error 传入一个参数会匹配 第一个函数声明,提示's'不能赋给number[]
handleData('s', 's') // error 匹配第二个函数声明, 提示's'不能赋给number类型
开头也说了,TypeScript重载更像是说明文档。为了给开发者看,方便开发者知道该怎么调用。
复制代码