TypeScript 学习总结 函数 接口 (二)

结构检查

再讲接口前,首先要理解,TS经常会对数据结构进行检测

摘抄官网的一句话 :TypeScript的核心原则之一是对值所具有的结构进行类型检查

通过代码来理解这句话。

function man(person: {name: string, age: number}) {
      console.log(person.name, person.age)
}
复制代码

上面这个man函数的参数person只能接受带有name: stringage: 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重载更像是说明文档。为了给开发者看,方便开发者知道该怎么调用。
复制代码

转载于:https://juejin.im/post/5cff4395e51d45109725fe55

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值