Typescript type进阶

一、背景

这是一篇自己总结的 Typescript type相关的进阶文章,适合有一定ts基础,并在type编写方面感到迷惑、感到绝望的同学,也给那些入门Typescript已久,却无法更上一层楼的童鞋一个方向。如果是Typescript小白,建议先看看基础知识,大神请忽略。不知道你有没有过这样一种错觉,Typescript使用也不算少了,各种interface各种type也写得得心应手。但他看起来重复又累赘,一点也不像别人家的type那么眉清目秀。年轻人,是时候进阶一波了,加油!相信你认真看完这篇文章,一定能够原地拔高3米!拔不高的当我没说~

另外,文章不足之处,还请各位大佬指正。

二、进阶姿势---从内置工具类型说起

Typescript内置工具type

  • Partial<T>,属性可选,使用频率:一般;

    看起来十分简单,通过keyof拿到泛型T的全部properties,再给每个property加上可选标记?即可。

    type Partial<T> = {
        [P in keyof T]?: T[P];
    };

    举个例子:我们的用户信息包含nameaddress属性,但是有些用户很特殊,他们这两个属性可有可无。

    type User={
        name:string;
        address:string;
    };
    type OptionalUser=Partial<User>
  • Required<T>属性为required,使用频率:一般;

    type Required<T> = {
        [P in keyof T]-?: T[P];
    };

    还是上面的例子,现在我们倒过来,让optional的属性变成required

    type OptionalUser=Partial<User>;
        type RequiredUser=Required<User>;

    其实和User是一样的,默认就是required

  • Readonly<T>属性只读,使用频率:一般;

    确保属性是只读的,不可以被修改,常用于react组件的propsstate

    type Readonly<T> = {
        readonly [P in keyof T]: T[P];
    };
  • Pick<T K extends keyof T>选取部分属性生成新type,使用频率:较多;这个helper用的就比较多了。
type Pick<T, K extends keyof T> = {
            [P in keyof T]: T[P];
        };

还是之前的User,我们现在多了一种用户,他只有name属性。这时候我们又不想重新写一个差不多的type,怎么办呢?

type NameOnlyUser=Pick<User,'name'>;// type NameOnlyUser = {name: string;}
  • Record<K extends keyof any, T>使用频率:一般;

    看起来就是创建一个具有同类型属性值的对象。没实际遇到使用的情况。

type Record<K extends keyof any, T> = {
        [P in K]: T;
    };
  • Exclude<T,U>使用频率:较多;

    从类型T中剔除所有可以赋值给U的属性,然后构造一个类型。
    主要用在联合类型的exclude中

    type Exclude<T, U> = T extends U ? never : T;
  • Extract<T,U>使用频率:一般;

    基本同上,功能相反

    type Extract<T, U> = T extends U ? T : never;
  • Omit<T, K extends keyof any>使用频率:较多;

    type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

    主要用于剔除interface中的部分属性。还是之前的User,现在我们想剔除name属性,当然可以使用前述的方式

    type UserWithoutName=Pick<User,'address'>;// type NameOnlyUser = {address: string;}

    这样就很麻烦,特别是属性比较多的时候,更简便的就是直接用Omit

    type UserWithoutName=Omit<User,'name'>;// type NameOnlyUser = {address: string;}
  • NonNullable<T>使用频率:一般;

    type NonNullable<T> = T extends null | undefined ? never : T;

    主要用于过滤掉null和undefined两个基本类型的数据。

  • Parameters<T extends (...args: any) => any>使用频率:一般;

    写到这里恰好就进入了type中比较有趣的地方了,为什么?因为之前的type都没有用到infer这个关键字,而之后的几个type全用到了,纯属巧合,不过infer确实是type进阶最重要的知识点之一。这个type获取了泛型T的函数参数:

    type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

    具体来说,我们可以通过react-router里面常用的withRouter函数举例,看看我们的Parameters返回结果(原理放在下一节):

    export function withRouter<P extends RouteComponentProps<any>>(component: React.ComponentType<P>): React.ComponentClass<Omit<P, keyof RouteComponentProps<any>>>;// 这是withRouter的函数签名,现在我们来拿他试试
    type WithRouterParameters=Parameters<typeof withRouter>//type WithRouterParameters = [React.ComponentType<RouteComponentProps<any, StaticContext, any>>]

    这样,我们就顺利拿到了我们的参数type。

  • ConstructorParameters<T>获取构造函数参数类型,使用频率:一般;

    type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

    这个看起来和上面的几乎一样,其实也差不多,原理是一模一样的。
    我们还是用例子来说话,我们定义一个User的class,通过这个type来获取class的constructor parameters

    class User {
        static name: string
        static address: string
        static age: number
        constructor(name: string, address: string, age: number) {
            this.name = name;
            this.address = address;
            this.age = age
        }
        public sayHello() {
            alert('hello from' + this.name)
        }
    };
    type ConstructorParametersOfUser=ConstructorParameters<typeof User>//type ConstructorParametersOfUser = [string, string, number]

    轻松就拿到了实际的参数tuple:[string, string, number]

  • ReturnType<T>获取函数返回值类型,使用频率:一般;

    type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
  • InstanceType<T>获取实例类型,使用频率:一般;

    type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

三、extends and infer

为什么要单独说说这两个关键字,仔细看过文章上面部分的同学会发现,几乎所有使用infer的地方,都有extends的身影。上面部分看完且理解的同学几乎不用继续看了,上面没看完,心态有点爆炸,脑子有点懵的同学,真是难为你们了,现在来讲 重点

3.1 extends(有条件类型)

TypeScript 2.8 introduces conditional types which add the ability to express non-uniform type mappings. A conditional type selects one of two possible types based on a condition expressed as a type relationship test:
T extends U ? X : Y

大致意思就是:TypeScript 2.8引入了有条件类型,它能够表示非统一的类型。 有条件的类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一。再简化一点,若T能够赋值给U,那么类型是X,否则为Y。
这个好理解,我们经常export class CustomComponent extends React.Component<any, any> { }就是CustomComponent可赋值给React.Component(type)。

3.2 infer(类型推断)

Within the extends clause of a conditional type, it is now possible to have infer declarations that introduce a type variable to be inferred. Such inferred type variables may be referenced in the true branch of the conditional type. It is possible to have multiple infer locations for the same type variable.

文档意思是说,现在在有条件类型的extends子语句中,允许出现infer声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的true分支中被引用。 允许出现多个同类型变量的infer。
还记得上面的ParametersConstructorParametersReturnTypeInstanceType吗?他们正是利用了类型推断,获取各种待推断的类型变量。只要记住并理解了以下几点,你就已经完全掌握了infer:

  • 只能出现在有条件类型的extends子语句中;
  • 出现infer声明,会引入一个待推断的类型变量
  • 推断的类型变量可以在有条件类型的true分支中被引用;
  • 允许出现多个同类型变量的infer。

为了便于理解,我们先看这个小栗子:

type GetTypeSimple<T>=T extends infer R ? R : never;

emmmm?这是什么操作?
这个GetTypeSimple接收一个T作为参数来判断他的推断类型,显然,种瓜得瓜种豆得豆,传什么类型就是什么类型。

type Test1 = GetTypeSimple<number>;//number
type Test2 = GetTypeSimple<string>;//string
type Test3 = GetTypeSimple<Array<number>>;//number[]
type Test4 = GetTypeSimple<typeof withRouter>;//<P extends RouteComponentProps<any, StaticContext, any>>(component: React.ComponentType<P>) => React.ComponentClass<Omit<P, "history" | "location" | "match" | "staticContext">, any>

不知各位看官有点感觉没有?
继续,来个稍微复杂点的栗子:
我们假设定义一个如下的类型,要求将他的实例属性type不为never的单独拿出来作为一个联合类型,使用infer来完成。

class User {
public name: string
public address: string
public age: number;
public never:never;
constructor(name: string, address: string, age: number) {
    this.name = name;
    this.address = address;
    this.age = age
}
public sayHello() {
    alert('hello from' + this.name)
}
}

期望的结果是返回:string | number | (() => void)
看到这么个class,记性好的同学应该想到了上面的InstanceType,没错,这里我们借助它来推导返回的实例type;

type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
type TypeOfUser=InstanceType<typeof User>//User

然后对拿到的InstanceType进行属性类型的推导,最后过滤掉类型为never的属性即可

type TypeOfUserWithoutNever={
    [K in keyof TypeOfUser] : TypeOfUser[K] extends infer S ? S : never
}[keyof TypeOfUser]//`string | number | (() => void)`

最后,我们把这两步合到一起:

type GetUnionPropertiesWithoutNeverOfT<T extends new (args: any) => any> = T extends new (...args: infer R) => infer U
?
{
    [K in keyof U]: U[K] extends infer S ? S : never
}[keyof U]
: never;
type TypeOfUserWithoutNever=GetUnionPropertiesWithoutNeverOfT<typeof User>//`string | number | (() => void)`

看到这里的,基本上都能搞明白了,其实上面的GetUnionPropertiesWithoutNeverOfT我故意多写了一个推断类型R,验证了允许出现多个同类型变量的infer。其实完全没用,去掉即可。
那么,是时候来检验一波你们到底有没有掌握上面的知识了。

下面我们掌声有请2018年12月份的一道来自LeetCode-OpenSource笔试题
假设有一个叫 EffectModule 的类

class EffectModule {}

这个对象上的方法只可能有两种类型签名:

interface Action<T> {
    payload?: T
    type: string
}
asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>
syncMethod<T, U>(action: Action<T>): Action<U>

这个对象上还可能有一些任意的非函数属性:

interface Action<T> {
    payload?: T;
    type: string;
}

class EffectModule {
    count = 1;
    message = "hello!";

    delay(input: Promise<number>) {
        return input.then(i => ({
        payload: `hello ${i}!`,
        type: 'delay'
        });
    }

    setMessage(action: Action<Date>) {
        return {
        payload: action.payload!.getMilliseconds(),
        type: "set-message"
        };
    }
}

现在有一个叫 connect 的函数,它接受 EffectModule 实例,将它变成另一个一个对象,这个对象上只有EffectModule 的同名方法,但是方法的类型签名被改变了:

asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>> // 变成了
asyncMethod<T, U>(input: T): Action<U>
syncMethod<T, U>(action: Action<T>): Action<U>  //变成了
syncMethod<T, U>(action: T): Action<U>

connect 之后

const effectModule = new EffectModule();
const connected: Connected = connect(effectModule);
type Connected = {
    delay(input: number): Action<string>
    setMessage(action: Date): Action<number>
};// 期望结果

此处建议自己先试试解题,再来看我的思路和你的一不一样。

第一步,观察结果,发现除了函数属性,普通的属性都没了,看来我们要先过滤掉非函数属性

const effectModule = new EffectModule();
const connected: Connected = connect(effectModule);
type PickFunctionProperties<T>={
    [K in keyof T]:T[K] extends Function ? K:never        
}[keyof T];
type FunctionProperties=PickFunctionProperties<EffectModule>//"delay" | "setMessage"

然后通过Pick产出我们需要的只包含函数属性的类型

type FunctionsLeftT<T>=Pick<T,PickFunctionProperties<T>>;
type FunctionLeftT1=FunctionLeftT<EffectModule>
<!-- type FunctionLeftT1 = {
        delay: (input: Promise<number>) => Promise<{
            payload: string;
            type: string;
        }>;
        setMessage: (action: Action<Date>) => {
            payload: number;
            type: string;
        };
    } -->

紧接着,用我们今天所学,进行最关键的类型推导转换:
观察题目可以发现,两个函数签名参数和返回值都不太一样,所以我们需要先判断当前处理的函数是哪种类型,然后运用对应类型的转换规则就可以了,这里我写详细一些,方便大家真的搞懂这个点,
我们先来转换delay这个函数:

type TransformDelay<T extends (args: any) => any> = T extends (input: Promise<infer S>) => Promise<Action<infer U>> ? (input: S) => Action<U> : never

同理,我们再来转换setMessage这个函数:

type TransformSetMessage<T extends (args: any) => any> = T extends (action: Action<infer V>) => Action<infer U> ? (action: V) => Action<U> : never

然后,这两种类型的函数其实再来一个条件类型判断就完全可以写到一起,不信你看

Method extends delay ? TransformDelay : TransformSetMessage

于是我们可以写一个更加通用的type,假设我们命名为ConnectMethod

type ConnectMethod<T extends (args: any) => any> =
    T extends (input: Promise<infer S>) => Promise<Action<infer U>>
    ? (input: S) => Action<U>
    : T extends (action: Action<infer V>) => Action<infer U>
    ? (action: V) => Action<U>
    : never;

最后处理掉FunctionLeftT1的函数属性就可以啦!

type ConnectAllMethod<T> = {
    [K in keyof T]: ConnectMethod<T[K]>
}

所以最后,我们的Connect完成了,恭喜你通过面试(哈哈,别想太多)。

type Connect<T> = ConnectAllMethod<FunctionLeftT<T>>

最后按照题目意思,我们的Connect应该长这样:

type Connect = (module: EffectModule) => ConnectAllMethod<FunctionLeftT<EffectModule>>;
//具体使用

const connect:Connect// 省略函数具体实现。。。
//
const connected: Connected = connect(effectModule);

完事儿,吃根辣条冷静会儿。

四、总结

到这儿基本上想说的都说完了,个人认为Typescript的类型掌握到这个程度,已经可以hold住大部分场景了,技多不压身,也许你现在不会用到条件类型判断不会用到类型推断,但是多掌握一点儿肯定是好事,万一就需要用了呢,react官方的types里面就有大约20处用到inferextends自然是多不胜数了。

相关推荐