You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
typeCurryV1<Pextendsany[],R>=(arg: Head<P>, ...rest: Tail<Partial<P>>)=>HasTail<P>extendstrue
? CurryV1<Tail<P>,R> : R
这样一看, 似乎确实是支持非柯里化的多参数调用了, 测试一下:
// 定义一下柯里函数declarefunctioncurryV1<Pextendsany[],R>(f: (...args: P)=>R): CurryV1<P,R>constcurriedCb=curryV1(curryCb1)constcurriedCb2=curryV1(curryCb2)constr1=curriedCb('name')// okconstr2=curriedCb('name',1,true)// okconstr3=curriedCb('name',1,true)(1,false)// should be error
这篇文章主要参考了Learn Advanced TypeshScript, 加上自己查阅的一些资料和个人理解. 由于后半部分讲的是Ramda里的 placeholder, 个人不太常用 Ramda 就暂时跳过了.
以及, 这篇文章写的比较虚, 很多地方可能没有完全理解, 见谅.
从柯里化讲起
柯里化定义简单来讲, 就是一个接受 n 个参数的函数, 转换成每次只接受一个参数的函数, 最多需要 n 次调用完. 实现如下
两个点:
Function.prototype.bind
方法在每次绑定参数的时候会减少被绑定的函数(这里即callback
)的length
,bind
几个参数,Function.length
相应减少对应的数量. 传入的绑定参数依次以队列形式从左往右添加到callback
. 另这里 mdn 我其实没查到相关说明, 去ecma 手册才找到的...递归调用. 递归调用需要一个终止条件, 这里终止条件为当
args.length >= callback.length
, 停止接受实参绑定, 调用该回调. 即由于bind
, 每次callback
都在不断接受需要的参数, 其length
属性也不断减少, 并且最终会减少到 0, 那么当最后一次传递参数(比如现在callback.length=1
), 传了 2 个(args.length=2
), 那么第一个参数会被加入到回调函数中作为实参, 递归终止, 柯里化结束测试一下:
可以看到, 我们可以遵循严格的柯里化, 一次传入一个参数进行调用, 也可以不遵守, 传入多个参数调用.
这里需要注意一点: 允许
...d
在最后一次调用的时候传入无限个参数还能调用成功的原因是:Function.length
并不把 rest 参数计算在内: This number excludes the rest parameter and only includes parameters before the first one with a default value添加类型
overload 重载
根据声明函数时的不同一般有两种方法进行函数重载:
const
声明(使用type
或interface
声明重载类型)第一种:
第二种:
重载定义类型
观察上面的
curry
代码可以发现, 由于存在递归调用, 返回值可能是一个函数(本身), 或者是最后的结果, 因此可以尝试使用重载, 如下:很遗憾会出现如下类似错误:
查阅了一下, 发现一个类似的问题: Generic curry function with TypeScript 3.
个人理解是, 这个柯里化函数如果使用上述定义的话, 每次返回的函数的类型都是
F<T, R>
, 然而实际上随着每次调用传入参数之后其参数类型应该是不同的. ts 目前还不能把剩余泛型参数(Generic rest parameters)分割成更小的元祖类型, 也就是concatenate tuples目前还不完善解决方法一个是使用
heaps of overloads
, 也就是硬编码类型, 规定所有可能的情况, 也就是上述问题下的第二个答案做法, 我自己尝试了一下, 大致如下:然后可以定义到
curry
函数上:当然把上述重载抽成一个类型也是可以的, 如下:
翻阅了一下
lodash
里面关于curry
里的定义, 思路也是类似的, 唯一区别是里面用了一些占位符(placeholder), 这里不做深究:泛型
ts2.8引入了一些新概念, 比如 conditional types(
T extends U ? X : Y
),infer
, 一些工具类型例如(Parameters<T>
,ReturnType<T>
), 其本质都是为了方便做泛型类型推导泛型一个很明显的好处是可以在 run time 检查类型, 并且确定类型是一个具体的唯一的值, 比如拿官网的一个泛型例子:
假设不使用泛型:
可以看到推断出的类型是一个 union 类型, 并不精准
需求
假设不考虑具体代码实现, 仅仅想要实现这样一个
Curry
类型, 满足如下的类型推断:具体示例如下
基本概念
三个基本关键词:
type
,extends
,infer
type
: 自定义类型, 可以想象成一个 函数, 等号左边可以接受输入(也就是泛型), 可以想象成这里泛型充当这个"函数"的参数, 等号右边是输出, 也就是最后会返回的类型, 比如类似这样extends
: 可以看成js
中的===
, 用于比较. 比如type T0<T> = T extends U ? X : Y
的意思是, 泛型T
是否和U
类型"相等", 如果相等的话type T0
返回X
类型, 否则返回Y
类型infer
: 可以理解成一个变量, 用来代表一些泛型, 后面有例子会讲到泛型类型和泛型函数(generic type & generic function)
假设有以下两个类型:
第一个可以看做就是一个泛型类型(
generic type
), 接受一个T
作为参数. 第二个可以看做是一个普通的泛型类型(generic function
), 不接受任何泛型输入, 返回的就是一个泛型函数. 测试一下:具体想要更详细的参考可以去看这个问题下的回答
元祖类型
直接引用官网的定义:
泛型基本类型推导
Params
定义: 以元组形式返回一个函数的参数:
理解: 接受一个参数
F
类型, 该类型满足(...args: any[]) => any
的函数类型, 判断条件为F
是否满足((...args: infer A) => any)
, 满足返回A
类型, 否则返回never
类型伪代码如下:
测试一下:
官方也有一样的实现: Parameters
不过注意, 这里虽然给
extends
后面的条件加上括号不是必须的, 但是建议加上, 遇到更加复杂的类型推导的时候也更容易看清, 后面会有具体例子说明这一点Head
定义: 给定一个元祖类型, 返回该元祖里面的第一个类型
测试一下:
Tail
定义: 给定一个元祖类型, 返回该元祖类型里第一个类型之后的所有类型
测试一下:
HasTail
定义: 给定一个元祖类型, 返回这个元祖类型是否能满足 Tail 类型(其类型数量是否 > 1)
测试一下:
上面三个类型和我们实现一个柯里化类型其实是有一定的关系的:
Head
: 比较经典的柯里化里面, 一个柯里化过后的函数每一次只接受一个参数, 因此Head
类型可以帮助检查每次需要接受的一个参数是哪个Tail
: 每次调用一次柯里化的函数, 正常讲一个参数就已经被消耗(consumed)掉了, 因此需要移入下一个参数,Tail
类型可以帮助判断下一个所需要的参数类型是哪个HasTail
: 回顾上面的柯里化函数实现, 无限递归是需要有一个终止条件的, 也就是所有需要给定的参数都被消耗完毕,HasTail
正好用来判断这点ObjectInferValue
定义: 给定一个对象和键, 返回该键下的值类型
测试一下:
ObjectInferKey
定义: 给定一个对象和值, 根据值得到所对应的键类型
这里的推断思路是这样的: 先算出
{}[]
, 也就是对应的O[K
], 而O[k] extends V
形成一个条件, 也就对这个结果进行再一步判断, 看是否和V
相等, 相等返回推导结果是K
伪代码大致为:
测试一下:
FunctionInfer
定义: 给定一个函数, 返回该函数的参数和返回值
测试一下:
PromiseInfer
定义: 返回 promise 的类型
测试一下:
ArrayInfer
定义: 返回一个数组里的类型
测试一下:
TupleInfer
定义: 给定一个元祖类型, 返回第一个类型以及后面元素的联合类型
测试一下
测试
可以自定义简单的类型测试函数/类型帮助我们进行测试
Equals
定义: 判断两个类型是否相等, 相等返回
true
, 否则返回false
使用方法如下:
assertType / assertNotType
两个函数分别用于做类型检测
举个例子, 需要测试之前实现的
Head
类型:编译器不报错就说明没类型是正确的
柯里化类型 1
至此根据上面定义的一些工具类型已经可以写出比较基本的柯里化类型了
CurryV0
实现如下:
解析: 这里接受两个参数,
P
(所需要接收的所有参数)和R
(返回结果), 判断条件为HasTail<P>
是否和true
相等, 相等返回输出(arg: Head<P>) => CurryV0<Tail<P>, R>
这样的一个函数类型, 否则返回(arg: Head<P>) => R
的函数类型(也就是最后一次接受参数的形态)伪代码解析一下:
分析一下实现思路: 根据传统的柯里化定义, 每次只接受一个参数, 直到最后所有参数接收完毕返回结果. 那么
Head<P>
即返回每次所需要的那个参数, 每次调用(传入参数)之后,Tail<P>
做切片处理, 帮助去除之前已经传入的参数, 返回还未被传入的参数列表.HasTail
帮助判断是否还有参数等待被传入测试一下:
这里定义一下两种需要被柯里化的函数, 下面所有的测试均使用这两个函数:
定义
curry
函数, 这里使用declare
仅指定类型, 不做具体实现, 该类型会在 run-time 被删除可以看到进行传统的一个参数的柯里化调用的时候很完美, 包括完整的类型推断
然而似乎并不接受非柯里化类型的多个参数调用,
...rest
也不支持CurryV1
改进一下, 如下:
这样一看, 似乎确实是支持非柯里化的多参数调用了, 测试一下:
可以发现, 尽管改进之后允许传入多个参数, 但是对于第三个测试用例在已经传入了
number
和boolean
类型之后本应该返回R
的, 似乎返回的类型还是一个函数类型, 且实质和仅传入了一个参数的效果是一样的仔细看实现发现, 实际上还是使用了
Head
和Tail
进行参数校验, 也就是说不管传入了多少参数, 由于我们使用Head
强制要求传入了第一个参数,Tail
永远只会抹除第一个传入的参数, 返回剩余的参数数组.CurryV2
继续改进, 如下:
这里不考虑使用
Head
了, 直接使用T
来跟踪所有被传入的参数,Tail<T>
确实是可以根据数量"正确"返回剩余所需要的参数的但是这里有个问题, 使用了
any[]
实际上失去了所有的类型检查, 比如下面的测试:显而易见,
curryCb1
要求的第一个参数应该是string
类型, 但是传入number
类型好像也不报错, 可以看一下具体的类型由于
any[]
的存在, 失去了类型检测. 因此目前来看还需要做更多递归类型与其他泛型推导
根据之前的实践发现, 现在需要一些工具类型来帮助追踪已经传入的参数, 也就是说, 没错调用之后需要明确知道有哪些参数已经被传入进去(消耗掉的), 还有哪些参数等待被传入. 接下来的工具函数都是为了帮助做到这点
Last
定义: 返回元祖类型的最后一个元素
简单讲一讲推断思路: 最后返回的是 {}[], 也就是该对象的结果, 其中该对象的
key
需要判断, 判断条件是T
中元素是否> 2
, 是继续递归调用, 同时T
使用Tail<T>
进行数量缩减, 否则返回最后的元素伪代码为:
测试一下:
Length
定义: 返回一个元祖类型的长度
测试一下:
Prepend
定义: 给定一个类型和一个元祖类型, 在该元祖类型的头部插入这个类型, 输出该新元祖类型
测试一下
Drop
定义: 给定一个数字 N, 一个元祖类型, 返回从索引从数字之后(包括)的所有类型
简单讲一讲推导思路: 给定一个
I
为空元祖, 每次Drop
一次就往里面添加一个元素, 不断比较N
和I
的长度, 如果I
的长度< N
继续递归调用, 否则返回最终结果测试一下:
Cast
定义: 检查 X 是否是 Y 类型, 如果满足保持 X 类型, 否则返回 Y 类型
测试一下:
柯里化类型 2
讲一讲如何追踪已经传入的参数的思路, 假设有如下两个类型
parameters
和consumed
, 分别代表总的(一开始)需要的参数元祖类型和已经传入(consumed)的参数元祖类型:配合之前的工具类型可以轻松得到剩余需要的类型列表
CurryV3
根据上面的思路可以实现第三版:
简单讲一下推导思路: 核心在于
Drop<Length<T>, P>
, 这个类型代表的就是剩余需要被传入的参数, 利用Length
进行判断是否还存在参数等待被传入然而实现的时候会存在错误:
这时需要使用之前提到的
Cast
帮助修复CurryV4
测试一下:
似乎对于严格的单个参数柯里化调用和非严格的函数调用都没有问题. 然而, 假如尝试加入
...rest
参数出现了两个问题:
curriedCb2('name', 1)('f')('a', 'aaa')
这样的调用方式其实是错误的, 正确方式应该是curriedCb2('name', 1, 'f', 'a', 'aaa')
也就是...rest
参数应该是随着最后一个参数一起传入完毕, 不应该返回出一个新的函数类型来接受...rest
参数boolean
结果了原因其实是这样,
Length
类型是无法推导出...rest
的参数个数的, 毕竟他无法确切知道你需要传入的参数到底是有几个推导结果是
number
....其实是一个挺不错的结果了CurryV5
这里使用
[any, ...any[]]
来进行判断是否是最后一次非...rest
参数的传入, 同时配合Cast
保证返回正确类型测试一下:
算是基本实现了功能
占位符(Placeholders)
个人不太熟悉
Ramda
, 不做深究, 不过lodash
源码里面也提到了一些, 这里仅做一个简单了解下面几种调用是等价的:
这里的
_
就是作为一个占位符, 即 placeholder, 或叫作 gap这块有兴趣还是建议去看作者原文
关于原文和源码
作者最后提到, 这篇文章其实是ts-toolbelt这个 repo 的入门教程, 这个 repo 涵盖了很多高级实用的 typescript 类型定义, 欢迎 star
以及, 作者将源码也放到了github上, 增加了一些新的内容, 比如
pipe
等新的类型定义, 可以自己 clone 下来试一试再次感谢作者@pirix-gh
参考
The text was updated successfully, but these errors were encountered: