typescript实现一个connect方法

2019 / 11 / 16

题目

typescript类型本身的学习难度不大,看完文档很快就能上手了,但是要想真正的会用类型编程就很难了,并不是说语法难或者不容易理解,而是说要从javascript的业务逻辑中走出来,把类型作为一种编程很难。

leetcode有一道比较有名的typescript的问题,这个题目主要就是解决redux和thunk的一些问题的。如果写过redux的话可以发现它的typescript写起来是真的比较麻烦。

题目可以看这里

理解题意

理解题意,题目很长大概意思是这样的。

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

class EffectModule {
count = 1
message = 0

// 输入一个promise,转化为另一个promise
delay(input: Promise<number>) {
return input.then(i => ({
payload: `hello, ${i}`,
type: 'delay',
}))
}

// 输入一个action,转化为另一个action
setMessage(action: Action<Date>) {
return {
payload: action.payload!.getMilliseconds(),
type: 'set-message',
}
}
}

把上边的类实例话的对象经过connect方法处理之后会变成下边这样子的一个对象,就是把action和异步的过程都去掉了。

{
delay: (input: number) => {
return {
payload: `hello, ${input}`,
type: 'delay',
}
},

setMessage: (input: Date) => {
return {
payload: input.getMilliseconds(),
type: 'set-message',
}
}
}

那么connect的方法的type应该是这样的

type Connect = (module: EffectModule) => any

现在的问题就是描述这个any。

稍微总结一哈就是,拿到一个对象之后需要把对象的方法提取出来,并且让异步和action变成普通的payload。

所以问题点有两个

1. 提取一个对象的所有函数

2. 把一种函数类型变成另外一种

提取对象的函数方法

提取对象的某些键可以用Pick,提取函数的方法实现与Pick的实现类型。

首先一个需要熟知的拿到所有的对象的键的方法就是 K in keyof T,然后判断所有的键是否extends Function如果不是就变成never,也就是删除的意思。

这个类型的实现如下

type PickMethod<T> = { [K in keyof T]: T[K] extends Function ? T[K] : never }[keyof T]

普通人肯定一脸懵逼,其实懵逼很正常,这里面涉及了大多数人都不会用到一些知识点和语法。主要包括

1. 映射类型的操作符 in

2. 索引类型的操作符 keyof

3. 条件类型的操作符 extends

4. 底部类型 never

5. interface的取值

来一个一个说。

keyof用来操作一个对象并返回一个一个key的联合类型,比如

type Keys = keyof { a: boolean, b: string }

会返回一个 'a' | 'b'

in会对联合类型中的值进行遍历,并逐一操作

extends用来判断后者是否前者可以赋值给后者,或者说前者是后者的子类型,然后接一个三元运算符可以进行条件判断。

前半部分理解了之后可以得到一个返回值

大概像这样比如 { a: 'a', b: 'b', c: never },那么这个返回值要去取出键所有的值 'a' | 'b'。首先需要知道typescript中的never作为最底层的类型,所有的类型可以说都是包含他的,所以他并没有显示含义,经常用来作为不存在处理,比如 'a' | never === 'a',所以下边的的式子也是成立的。

{ a: 'a', b: 'b', c: never }['a' | 'b'| 'c'] // 'a' | 'b'

再解释一下,上边的方法可以认为是取出interface的键为a或者b或者c的值,never是不存在,所以结果就是字面量的a或者b。

转化方法的类型

提取好了方法之后,下一步就是转化方法,这个比较简单,需要把一个接受action或者promise包裹的action的两个方法

type SyncMethod<T, U> = (action: Action<T>) => Action<U>
type AsyncMethod<T, U> = (action: Promise<T>) => Promise<Action<U>>

变成直接输入payload的方法,并且返回正常的action的方法

type SyncMethodConnect<T, U> = (input: T) => Action<U>
type AsyncMethodConnect<T, U> = (input: T) => Action<U>

然后实现一个把方法转化为Connect之后的方法的工具,实现出来是下边这个样子。

type EffectModuleMethodsConnect<T> = T extends SyncMethod<infer U, infer V>
? SyncMethodConnect<U, V>
: T extends AsyncMethod<infer U, infer V>
? AsyncMethodConnect<U, V>
: never

意思就是先判断是不是SyncMethod,是的话返回SyncMethodConnect,否则判断是不是AsyncMethod,是的话返回AsyncMethodConnect,否则返回never,就是把除了这两种以外的方法剔除了。

其中运用了一个infer关键字,infer是一个表示后边的类型变量是个待推断类型,就是说这个类型我们不知道他是什么,但是有想要用它。比如说上边我们并没有声明 U 和 V 这两个参数,但是我们需要他,就把它声明为待推断类型,然后通过推断来得出他是什么。

举个实用而又相对简单的例子,写一个类型用来获取函数参数的类型

type ParamType<T> = T extends (param: infer P) => any ? P : never

就是说参数infer P,不需要知道参数的类型是什么,只需要返回这个参数类型就好了,typescript在输入一个函数的时候会自动帮我们推断出来,上边的例子也一样,在输入T之后会自动推断出来U和V的类型。

最后融合一下提取函数类型和转化方法两个能力,就能实现一个完美的connected类型了。

type Connected<T> = { [M in MethodPick<T>]: EffectModuleMethodsConnect<T[M]> }
type Connect = (module: EffectModule) => Connected<EffectModule>

嗨,请先 登录

加载中...
(๑>ω<๑) 又是元气满满的一天哟