阅读 2199

TypeScript-从函数返回类型开始讲怎么用好TS

前言: 对于初学TS的同学,最大问题是不擅长处理函数的返回类型,如果一个函数返回的类型是多种或可以为空的,甚至是复合的,或者对原始类型做修改,做组合,做修剪,新手往往需要大量定义相似度高的类型来解决问题。而且单一的类型限定或使用范型,往往容易出现各种类型错误。往往缺少耐心的同学,就会直接用any类型。这样就违背TS使用的初衷了。甚至跟用JS没有什么区别了。搞清楚多种返回类型的技巧和写好工具辅助类型,才是用好TS的关键。

一.使用联合类型和交叉类型

比如我们有这样一个js函数

//js代码示例
function fucExp(){
    if(...条件){
        return {"aa":123}
    }else{
        return [1,2,3]
    }
}
复制代码

这个fucExp函数既可能会返回一个对象,也可能返回一个数组,如果用TS的话,返回类型应该怎么做呢?

1618483764-24542.jpeg

这个时候我们应该要用到TS的联合类型了,采用|符号定义 ,我们可以首先定义这个联合类型type,当然也可以直接使用。推荐使用下面这种写法。

//ts代码示例
type UncertaintyType = Object | number[] 

function fucExp(): UncertaintyType {
    if (0 < 3) {
        return { "aa": 123 }
    } else {
        return [1, 2, 3]
    }
}
复制代码

比如我有这样一个函数,需要合并了两个对象。返回的类型是两个对象的合并类型。我们如何定义他TS的类型呢?

//JS代码示例
function funcMerge() {
    let dog = {
        name: "jack",
        age: 23,
    }
    let behavior = {
        bark: () => {
            console.log('wang wang')
        }
    }
    return Object.assign(dog, behavior)
}
复制代码

再次强调,千万不要用any !!!

我们上交叉类型。交叉类型把几个类型的成员合并,使用&符号,可以形成一个拥有这几个类型所有成员的新类型。从字面上理解,可能会误认为是把取出几个类型交叉的(即交集)成员 。(注意:交叉类型是不交集,是合并,这里比较容易错,重点提醒) 正确操作如下

//TS代码示例
interface Behavior {
    bark: Function
}
interface Dog {
    name: string,
    age: number
}
type MergeType = Dog & Behavior
function funcMerge(): MergeType {
    let dog: Dog = {
        name: "jack",
        age: 23,
    }
    let behavior: Behavior = {
        bark: () => {
            console.log('wang wang')
        }
    }
    return Object.assign(dog, behavior)
}
复制代码

说明:根据你函数实际返回情况&和| 这两种类型都可以混合使用。 如果一个类型里面&和|使用过多,也很恶心,对于更复杂的类型,请看后面的工具类型的详细讲解。

二. never类型

如果我们有一个函数没有任何返回,那怎么定义它的返回类型呢? 你可能马上就会想到JS与此类似的 void 关键字,当一个函数返回空值时,它的返回值为 void 类型,但是,当一个函数永不返回时。在TS中可不能用void。比如我们这个函数只是执行的过程中,发生意外抛出一个错误而已。

TS中never类型会比较优雅的处理这种情况。比如看下面这段代码示例

//TS代码示例
function neverFunc():never{
    throw new Error('Throw my error');
}
复制代码

注意:但是除了 never 本身以外,其他任何类型不能赋值给 never

三. 类型捕获

比如有这样一种情况,我们从外部引入的json数据,或从其他第三方lib导入的Object,并没有定义类型,我们需要返回它的类型,应该怎么办呢?

在TS中可以通过 typeof 操作符在类型注解中使用变量。这允许你告诉编译器,一个变量的类型与其他类型相同。代码如下所示

//TS代码示例
let obj = {
    msg:"no"
}
function unknowReturnFunc() :typeof obj { //typeof 捕获对象类型
    obj.msg = "ok" 
    return obj
}
复制代码

不仅能够捕获对象的类型,也可以捕获成员的类型。代码如下所示

//TS代码示例
let obj = {
    msg:"no"
}
function unknowReturnFunc() :typeof obj.msg { //typeof 捕获成员类型
    obj.msg = "ok" 
    return obj.msg
}
复制代码

还有一种情况,我们希望函数返回的类型,限定在指定值的范围,比如我们有animal类型,里面有dog,有cat。 那么返回类型只能是这两种的其中一种,不能是其他的字符串,那么应该怎么做呢?

TS中keyof操作符能让你捕获一个类型的键,这允许你很容易地拥有像字符串枚举,常量这样的类型使用。那么请看下面的代码。

//TS代码示例
let animal ={
    dog:"dog",
    cat:"cat"
}
type AnimalType = keyof typeof animal
function funcKeyOfExp():AnimalType {
    let animalObj :AnimalType 
    animalObj="dog" //ok
    animalObj="cat" //ok
    //animalObj = "bird" //error  这个类型不被允许
    return animalObj
}
复制代码

从上面例子可以看出,可以很轻易的就实现了一个类似枚举的类型。其实这个就是利用了TS字面类型的特点。具体细节可以去官方手册查看。这里不作详细说明

四. 混合和多重继承

在TS中class不支持多重继承,而且TS中implements只能继承属性,不能继承代码逻辑 。 所以怎么实现呢 。利用函数返回一个扩展构造函数的新类,可以用TS混入的概念模拟多重继承。比如我们有个函数,传入一个类型,需要返回一个新类型,这个新类型是基于老类型的扩展。这是一个mixins操作。

话不多说,看例子:

//TS代码示例
type Constructor<T = {}> = new (...args: any[]) => T;

function userOne<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        myName = "Felix"
    }
}
function userTwo<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        score = 60;
        updateScore() {
           this.score =100;
        }
    };
}
class Person {
    age =20;
}
const UserTwo = userTwo(userOne(Person));
const userTwoInstance = new UserTwo()
userTwoInstance.myName
userTwoInstance.updateScore()
userTwoInstance.age 
//UserTwo 这个class拥有了其他类的所有方法和属性,巧妙的实现了多重继承
复制代码

通过上面的例子可以看出,函数利用范型继承,返回一个新类型再配合mixins操作。可以很轻松实现一个多重继承。这个就是TS和混合的概念。

五. 善用几个常用的工具类型

  1. Partial<T>构造可选的类型

比如我们有一个函数,需要返回一个定义好的类型,但是我们又不希望返回整个类型里面所有字段,只是部分字段,但我们希望不要对已有类型做修改。那应该怎么做呢?这个时候我们就要用到Partial这个工具类型了。

//Partial内部实现原理
type Partial<T> = {
    [K in keyof T]?: T[K] //利用keyof对类型所有的字段加可选属性
}
复制代码

那我们如何使用这个Partial呢? 看下面例子

interface UserInfo {
    id:number,
    name:string,
    mail:string,
}
function getUserPartail():Partial<UserInfo>{
    const userInfo ={
        id:123
    }
    return userInfo 
    //只初始化了id这个属性,也不会报错,如果返回类型不是Partial,肯定会报错,大家可以试一试
}
复制代码
  1. Require<T>构造必填的类型返回

当然存在Partial可选也会存在Require必选。首先我们看一下Require类型的定义

type Require<T> = {
    [P in keyof T]-?: T[P];
}
复制代码

你会发现,跟上面的Partial类型很相似,在?前面加了一个减号, 这个-?的作用是在于对原始类型属性里面的可选属性进行剔除,从而变为一个强制传入的一个属性。上面的例子我们使用Require改造一下。

interface UserInfo {
    id:number,
    name:string,
    mail:string,
}
function getUserPartail():Partial<Require>{
    const userInfo ={
        id:123,
        name:'felix',
        mail:'xxx@xx.com'
    }
    return userInfo 
    //这样我们返回的类型,必须是每个属性都要有。
}
复制代码

这个Require的作用,可以在我们calss中提供给外部使用的函数,我们可以限定某些参数的属性必须要有,可以将错误卡在编译这一层,不用走的运行时。减少我们代码实际运行过程中出错的概率。

3.Pick<T, K> 从类型T中挑选部分属性K来构造新的类型

Pick类型是一个很实用的类型。当我们返回的类型的属性某一个字段的是我们不想要的话,我们可以用Pick来剔除这个属性,而不是让它变为可选的,这样可以保证结构的干净和准确性。请看下面例子

interface UserInfo {
    id:number,
    name:string,
    mail:string,
}
//辅助工具用来剔除某个属性
function deleteProperty<T>(obj: T, key:keyof T ): T {
    const { [key]: deleted, ...newState } = obj;
    return newState as T
}

function getUserPick(userInfo:Pick<UserInfo, 'id' | 'name'>){
    console.log(userInfo) // 输出 {"id": 123,"name": "felix"} 
    return userInfo
}

getUserPick(deleteProperty({
    id:123,
    name:'felix',
    mail:'xxx@xx.com'
},'mail'))
复制代码

我们在传整个UserInfo类型的时候,是不符合要求的,会提示错误,因为Pick这里只允许传id和name两个字段的属性,所以我们利用deleteProperty这个函数工具辅助删掉mail这个字段。 这样返回一个新的类型,这样就不会报错了。大家可以自己跑一遍代码,会有更清晰的理解。

  1. Record<K, T> 构造一个类型,其属性名为K,属性值为T
type Record<K extends keyof any, T> = {
   [P in K]: T; 
}
复制代码
  1. Exclude<T, U> 从类型T中,剔除所有能赋值给U的属性
type Exclude<T, U> = T extends U ? never : T;
复制代码
  1. Extract<T, U> 从类型T中提取所有可以赋值给U的类型
type Extract<T, U> = T extends U ? T : never;
复制代码
  1. Omit<T, K> 从类型T中剔除所有能赋值给K的属性
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
复制代码
interface UserInfo {
    id:number,
    name:string,
    mail:string,
}
type UserOmit = Omit<UserInfo, 'mail'>
function getUserPick():UserOmit{
    return {
        id:123,
        name:"myName"
    }
}
复制代码

其实这个Omit跟Pick有点类似,Pick是提取并保留,Omit是提取并剔除。一个相反的概念,但也比较好理解。

  1. ReturnType<T>由函数类型T的返回值类型构造一个类型
type ReturnType<T extends (...arg: any) => any> = T extends (...arg:any) => infer R ? R : any;
//infer R 表示待推断的函数返回值。如果T能够赋值给(...arg:any) => infer R则结果是R,否则是any
复制代码

六. 结语

工具类型非常多,而且可以相互组合,比如Omit类型就用到了Pick和Exclude的组合。这里我就不一一介绍了,师傅领进门,修行靠个人。当然如果遇到非常好用的工具类型,我会继续补充。当然GITHUB有很多有用的工具类型库,个人推荐ts-toolbeltutility-types 大家可以去了解了解。掌握这些类型技巧,才能写出更Professional的TS代码。

文章分类
前端
文章标签