Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

简单聊一聊柯里化和 TypeScript 泛型 #9

Open
hacker0limbo opened this issue Mar 29, 2020 · 0 comments
Open

简单聊一聊柯里化和 TypeScript 泛型 #9

hacker0limbo opened this issue Mar 29, 2020 · 0 comments
Labels
typescript typescript 笔记整理

Comments

@hacker0limbo
Copy link
Owner

这篇文章主要参考了Learn Advanced TypeshScript, 加上自己查阅的一些资料和个人理解. 由于后半部分讲的是Ramda里的 placeholder, 个人不太常用 Ramda 就暂时跳过了.

以及, 这篇文章写的比较虚, 很多地方可能没有完全理解, 见谅.

从柯里化讲起

柯里化定义简单来讲, 就是一个接受 n 个参数的函数, 转换成每次只接受一个参数的函数, 最多需要 n 次调用完. 实现如下

function curry(callback) {
  return (...args) => {
    if (args.length < callback.length) {
      return curry(callback.bind(null, ...args))
    } else {
      return callback(...args)
    }
  }
}

两个点:

  • Function.prototype.bind方法在每次绑定参数的时候会减少被绑定的函数(这里即callback)的length, bind几个参数, Function.length相应减少对应的数量. 传入的绑定参数依次以队列形式从左往右添加到callback. 另这里 mdn 我其实没查到相关说明, 去ecma 手册才找到的...

    const foo = (arg1, arg2, arg3) => arg1 + arg2 - arg3
    const bar = foo.bind(null, 1)
    bar.bind(null, ...[2, 3])() // 0
  • 递归调用. 递归调用需要一个终止条件, 这里终止条件为当args.length >= callback.length, 停止接受实参绑定, 调用该回调. 即由于bind, 每次callback都在不断接受需要的参数, 其length属性也不断减少, 并且最终会减少到 0, 那么当最后一次传递参数(比如现在callback.length=1), 传了 2 个(args.length=2), 那么第一个参数会被加入到回调函数中作为实参, 递归终止, 柯里化结束

测试一下:

const test = (a, b, c, ...d) => true

curry(test)(1)(2)(3) // 严格按照定义, 无 rest 参数
curry(test)(1)(2)(3, 4, 5) // 严格按照定义, 怎么任意数量的 rest 参数
curry(test)(1)(2, 3, 4) // 非严格调用, 有 rest 参数
curry(test)(1, 2, 3) //  非严格调用, 无 rest 参数

可以看到, 我们可以遵循严格的柯里化, 一次传入一个参数进行调用, 也可以不遵守, 传入多个参数调用.

这里需要注意一点: 允许...d在最后一次调用的时候传入无限个参数还能调用成功的原因是: Function.length并不把 rest 参数计算在内: This number excludes the rest parameter and only includes parameters before the first one with a default value

添加类型

overload 重载

根据声明函数时的不同一般有两种方法进行函数重载:

  • 普通函数声明
  • const 声明(使用typeinterface声明重载类型)

第一种:

function foo(a: number): number
function foo(a: string): string
function foo(a: any) {
  return a
}

第二种:

// 或者使用 type Foo = {...}
interface Foo {
  (a: number): number
  (a: string): string
}

const foo: Foo = (a: any) => {
  return a
}

重载定义类型

观察上面的curry代码可以发现, 由于存在递归调用, 返回值可能是一个函数(本身), 或者是最后的结果, 因此可以尝试使用重载, 如下:

interface Curry<T extends any[], R> {
  (...args: T): Curry<T, R> 
  (...args: T): R 
}

function curry<T extends any[], R>(callback: Curry<T, R>): Curry<T, R> {
  return (...args: T) => {
    if (args.length < callback.length) {
      return curry(callback.bind(null, ...args))
    } else {
      return callback(...args)
    }
  }
}

很遗憾会出现如下类似错误:

Type '(...args: T) => Curry<T, R> | Curry<any[], unknown>' is not assignable to type 'Curry<T, R>'...

查阅了一下, 发现一个类似的问题: Generic curry function with TypeScript 3.

个人理解是, 这个柯里化函数如果使用上述定义的话, 每次返回的函数的类型都是F<T, R>, 然而实际上随着每次调用传入参数之后其参数类型应该是不同的. ts 目前还不能把剩余泛型参数(Generic rest parameters)分割成更小的元祖类型, 也就是concatenate tuples目前还不完善

解决方法一个是使用heaps of overloads, 也就是硬编码类型, 规定所有可能的情况, 也就是上述问题下的第二个答案做法, 我自己尝试了一下, 大致如下:

interface Curry1<T1, R> {
  (): Curry1<T1, R>
  (t1: T1): R
}

interface Curry2<T1, T2, R> {
  (): Curry2<T1, T2, R>
  (t1: T1): Curry1<T2, R>
  (t1: T1, t2: T2): R
}

interface Curry3<T1, T2, T3, R> {
  (): Curry3<T1, T2, T3, R>
  (t1: T1): Curry2<T2, T3, R>
  (t1: T1, t2: T2): Curry1<T3, R>
  (t1: T1, t2: T2, t3: T3): R
}

然后可以定义到curry函数上:

function curry<T1, R>(fn: (t1: T1) => R): Curry1<T1, R>;
function curry<T1, T2, R>(fn: (t1: T1, t2: T2) => R): Curry2<T1, T2, R>;
function curry<T1, T2, T3, R>(fn: (t1: T1, t2: T2, t3: T3) => R): Curry3<T1, T2, T3, R>;
function curry(callback: any) {
  return (...args: any) => {
    if (args.length < callback.length) {
      return curry(callback.bind(null, ...args))
    } else {
      return callback(...args)
    }
  }
}

当然把上述重载抽成一个类型也是可以的, 如下:

interface Curry {
  <T1, R>(fn: (t1: T1) => R): Curry1<T1, R>;
  <T1, T2, R>(fn: (t1: T1, t2: T2) => R): Curry2<T1, T2, R>;
  <T1, T2, T3, R>(fn: (t1: T1, t2: T2, t3: T3) => R): Curry3<T1, T2, T3, R>;
}

const curry: Curry = (callback: any) => {
  return (...args: any) => {
    if (args.length < callback.length) {
      return curry(callback.bind(null, ...args))
    } else {
      return callback(...args)
    }
  }
}

翻阅了一下lodash里面关于curry里的定义, 思路也是类似的, 唯一区别是里面用了一些占位符(placeholder), 这里不做深究:

curry_lodash

泛型

ts2.8引入了一些新概念, 比如 conditional types(T extends U ? X : Y), infer, 一些工具类型例如(Parameters<T>, ReturnType<T>), 其本质都是为了方便做泛型类型推导

泛型一个很明显的好处是可以在 run time 检查类型, 并且确定类型是一个具体的唯一的值, 比如拿官网的一个泛型例子:

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const x = getProperty({ a: 1, b: 'b' }, 'a') // number

假设不使用泛型:

function getProperty<T, K extends keyof T>(obj: T, key: keyof T) {
  return obj[key];
}

const x = getProperty({ a: 1, b: 'b' }, 'a') // string | number

可以看到推断出的类型是一个 union 类型, 并不精准

需求

假设不考虑具体代码实现, 仅仅想要实现这样一个Curry类型, 满足如下的类型推断:

  • 能够满足一次只传入一个参数, 且有类型提示(推导)
  • 能够满足一次传入多个参数, 且有类型提示(推导)
  • 最后一次调用允许传入剩余参数(rest parameters), 且有类型提示(推导)

具体示例如下

// 仅适用 declare 声明 curry 函数的类型, Curry 是需要最后实现的类型
declare function curry<P extends any[], R>(f: (...args: P) => R): Curry<P, R>
// 需要被柯里化的函数
const toCurry = (a: string, b: number, c: boolean, d: ...args: string[]) => true
const curried = curry(toCurry)

curried('a')(1)(true, 'd', 'dd') // true
curried('a')(1)(2) // error
curried('a', 1)(true, 'd', 'dd') // true
curried('a')(1, true, 'd', 'dd') // true
curried('a')(1, true, 'd', 2) // error

基本概念

三个基本关键词: type, extends, infer

  • type: 自定义类型, 可以想象成一个 函数, 等号左边可以接受输入(也就是泛型), 可以想象成这里泛型充当这个"函数"的参数, 等号右边是输出, 也就是最后会返回的类型, 比如类似这样

    // 无泛型参数, 返回 string 和 number 的联合类型
    type Foo = string | number
    
    // 接受一个泛型参数 T, 返回类型为这个参数泛型本身
    type Self<T> = T
    
    const foo: Self<Foo> = 'foo'
  • extends: 可以看成js中的===, 用于比较. 比如type T0<T> = T extends U ? X : Y 的意思是, 泛型T是否和U类型"相等", 如果相等的话type T0返回X类型, 否则返回Y类型

  • infer: 可以理解成一个变量, 用来代表一些泛型, 后面有例子会讲到

泛型类型和泛型函数(generic type & generic function)

假设有以下两个类型:

type Identical1<T> = (x: T) => T
type Identical2 = <T>(x: T) => T

第一个可以看做就是一个泛型类型(generic type), 接受一个T作为参数. 第二个可以看做是一个普通的泛型类型(generic function), 不接受任何泛型输入, 返回的就是一个泛型函数. 测试一下:

const id1: Identical1<string> = (x) => x
const id2: Identical2 = (x) => x

id1('id') // string
id1(1) // error
id2('id') // string
id2(2) // number

具体想要更详细的参考可以去看这个问题下的回答

元祖类型

直接引用官网的定义:

Tuple types allow you to express an array with a fixed number of elements whose types are known, but need not be the same

type tuple = ['a', number, string[]]

const t1: tuple = ['a', 1, ['b', 'c']]
const t2 = (...args: tuple) => true
t2('a', 42, []) // true

泛型基本类型推导

Params

定义: 以元组形式返回一个函数的参数:

type Params<F extends (...args: any[]) => any> = 
  F extends ((...args: infer A) => any) ? A : never

理解: 接受一个参数F类型, 该类型满足(...args: any[]) => any的函数类型, 判断条件为F是否满足((...args: infer A) => any), 满足返回A类型, 否则返回never类型

伪代码如下:

const Params = (F) => {
  if (F === ((...args: infer A) => any)) {
    return A
  } else {
    return never
  }
}

测试一下:

const fn = (name: string, age: number, single: boolean) => true
Params<typeof fn> // [string, number, boolean]

官方也有一样的实现: Parameters

不过注意, 这里虽然给extends后面的条件加上括号不是必须的, 但是建议加上, 遇到更加复杂的类型推导的时候也更容易看清, 后面会有具体例子说明这一点

Head

定义: 给定一个元祖类型, 返回该元祖里面的第一个类型

type Head<T extends any[]> = 
  T extends [any, ...any[]] ? T[0] : never

测试一下:

Head<[string, number, boolean]> // string

Tail

定义: 给定一个元祖类型, 返回该元祖类型里第一个类型之后的所有类型

type Tail<T extends any[]> = 
  ((...args: T) => any) extends ((arg1: any, ...tail: infer A) => any) ? A : []

测试一下:

Tail<[string, number, boolean]> => [number, boolean]

HasTail

定义: 给定一个元祖类型, 返回这个元祖类型是否能满足 Tail 类型(其类型数量是否 > 1)

type HasTail<T extends any[]> = 
  T extends ([] | [any]) ? false : true

测试一下:

HasTail<[1, 2, string]> // true
HasTail<Tail<Tail<[1, 2, string]>>> // false => [string]

上面三个类型和我们实现一个柯里化类型其实是有一定的关系的:

  • Head: 比较经典的柯里化里面, 一个柯里化过后的函数每一次只接受一个参数, 因此Head类型可以帮助检查每次需要接受的一个参数是哪个
  • Tail: 每次调用一次柯里化的函数, 正常讲一个参数就已经被消耗(consumed)掉了, 因此需要移入下一个参数, Tail类型可以帮助判断下一个所需要的参数类型是哪个
  • HasTail: 回顾上面的柯里化函数实现, 无限递归是需要有一个终止条件的, 也就是所有需要给定的参数都被消耗完毕, HasTail正好用来判断这点

ObjectInferValue

定义: 给定一个对象和键, 返回该键下的值类型

type ObjectInferValue<O, K> = 
  K extends keyof O ? O[K]: never

测试一下:

ObjectInferValue<{ a: 1, b: '22' }, 'a'> // 1

ObjectInferKey

定义: 给定一个对象和值, 根据值得到所对应的键类型

type ObjectInferKey<O, V> = 
  { [K in keyof O]: O[K] extends V ? K : never }[keyof O]

这里的推断思路是这样的: 先算出{}[], 也就是对应的 O[K], 而 O[k] extends V 形成一个条件, 也就对这个结果进行再一步判断, 看是否和 V 相等, 相等返回推导结果是 K

伪代码大致为:

const ObjectInferKey = (O, V) => {
  const K = keyof O
  if (O[K] === V) {
    return K
  }
  return never
}

测试一下:

ObjectInferKey<{ a: 1, b: '22' }, '22'> // 'b'

FunctionInfer

定义: 给定一个函数, 返回该函数的参数和返回值

type FunctionInfer<F> = 
  F extends (...args: infer A) => infer R ? [A, R] : never

测试一下:

FunctionInfer<(a: number, b: string) => true> // [[number, string], true]

PromiseInfer

定义: 返回 promise 的类型

type PromiseInfer<P> = 
  P extends Promise<infer T> ? T : never

测试一下:

const p = new Promise<string>()
PromiseInfer<typeof p> // string

ArrayInfer

定义: 返回一个数组里的类型

type ArrayInfer<T extends any[]> = 
  T extends (infer U)[] ? U : never

测试一下:

ArrayInfer<typeof arr> => string | number
ArrayInfer<(string | number)[]> => string | number

TupleInfer

定义: 给定一个元祖类型, 返回第一个类型以及后面元素的联合类型

type TupleInfer<T> = 
  T extends [infer A, ...(infer B)[]] ? [A, B] : never

测试一下

TupleInfer<[string, number, boolean]> => [string, number | boolean]

测试

可以自定义简单的类型测试函数/类型帮助我们进行测试

Equals

定义: 判断两个类型是否相等, 相等返回true, 否则返回false

type Equals<X, Y> = 
  (<T>() => T extends X ? 1 : 2) extends 
  (<T>() => T extends Y ? 1 : 2) ? true : false;

使用方法如下:

Equals<[true, false], [true, false]> // true
Equals<[true, false], [true, 1]> // false

assertType / assertNotType

两个函数分别用于做类型检测

const assertType = <T extends true>() => {}
const assertNotType = <T extends false>() => {}

举个例子, 需要测试之前实现的Head类型:

type test = Head<[string, number, boolean]>

assertType<Equals<test, string>>()

编译器不报错就说明没类型是正确的

柯里化类型 1

至此根据上面定义的一些工具类型已经可以写出比较基本的柯里化类型了

CurryV0

实现如下:

type CurryV0<P extends any[], R> =
  (arg: Head<P>) => HasTail<P> extends true ? CurryV0<Tail<P>, R> : R

解析: 这里接受两个参数, P(所需要接收的所有参数)和R(返回结果), 判断条件为HasTail<P>是否和true相等, 相等返回输出(arg: Head<P>) => CurryV0<Tail<P>, R>这样的一个函数类型, 否则返回(arg: Head<P>) => R的函数类型(也就是最后一次接受参数的形态)

伪代码解析一下:

const CurryV0 = (P, R) => {
  if (HasTail<P> === true) {
    return (arg: Head<P>) => CurryV0<Tail<P>, R>
  }
  return (arg: Head<P>) => R
}

分析一下实现思路: 根据传统的柯里化定义, 每次只接受一个参数, 直到最后所有参数接收完毕返回结果. 那么Head<P>即返回每次所需要的那个参数, 每次调用(传入参数)之后, Tail<P>做切片处理, 帮助去除之前已经传入的参数, 返回还未被传入的参数列表.HasTail帮助判断是否还有参数等待被传入

测试一下:

这里定义一下两种需要被柯里化的函数, 下面所有的测试均使用这两个函数:

// 无 ...rest 参数的
const curryCb1 = (a: string, b: number, c: boolean) => true
// 有 ...rest 参数的
const curryCb2 = (a: string, b: number, ...c: string[]) => true

定义curry函数, 这里使用declare仅指定类型, 不做具体实现, 该类型会在 run-time 被删除

declare function curryV0<P extends any[], R>(f: (...args: P) => R): CurryV0<P, R>
const curriedCb = curryV0(curryCb1)
const curriedCb2 = curryV0(curryCb2)

const r1 = curriedCb('name')
const r2 = curriedCb('name')(1)
const r3 = curriedCb('name')(1)(true)

type test = typeof curriedCb
assertType<Equals<test, (arg: string) => CurryV0<[number, boolean], boolean>>>()
// 传统的只接受一个参数调用的都很完美

可以看到进行传统的一个参数的柯里化调用的时候很完美, 包括完整的类型推断

// 无法进行剩余多参数调用
const r4 = curriedCb2('name')(1)('a', 'b') // error
// 无法进行合并参数调用
const r5 = curriedCb2('name', 1) // error

然而似乎并不接受非柯里化类型的多个参数调用, ...rest也不支持

CurryV1

改进一下, 如下:

type CurryV1<P extends any[], R> = 
  (arg: Head<P>, ...rest: Tail<Partial<P>>) => HasTail<P> extends true 
    ? CurryV1<Tail<P>, R> : R

这样一看, 似乎确实是支持非柯里化的多参数调用了, 测试一下:

// 定义一下柯里函数
declare function curryV1<P extends any[], R>(f: (...args: P) => R): CurryV1<P, R>

const curriedCb = curryV1(curryCb1)
const curriedCb2 = curryV1(curryCb2)

const r1 = curriedCb('name') // ok
const r2 = curriedCb('name', 1, true) // ok
const r3 = curriedCb('name', 1, true)(1, false) // should be error

可以发现, 尽管改进之后允许传入多个参数, 但是对于第三个测试用例在已经传入了numberboolean类型之后本应该返回R的, 似乎返回的类型还是一个函数类型, 且实质和仅传入了一个参数的效果是一样的

仔细看实现发现, 实际上还是使用了HeadTail进行参数校验, 也就是说不管传入了多少参数, 由于我们使用Head强制要求传入了第一个参数, Tail永远只会抹除第一个传入的参数, 返回剩余的参数数组.

Somehow, we are going to need to keep track of the arguments that are consumed at a time

CurryV2

继续改进, 如下:

type CurryV2<P extends any[], R> = 
  <T extends any[]>(...args: T) => HasTail<P> extends true
    ? CurryV2<Tail<T>, R> : R

这里不考虑使用Head了, 直接使用T来跟踪所有被传入的参数, Tail<T>确实是可以根据数量"正确"返回剩余所需要的参数的

但是这里有个问题, 使用了any[]实际上失去了所有的类型检查, 比如下面的测试:

declare function curryV2<P extends any[], R>(f: (...args: P) => R): CurryV2<P, R>

const curriedCb = curryV2(curryCb1)

const r1 = curriedCb(1) // should be error

显而易见, curryCb1要求的第一个参数应该是string类型, 但是传入number类型好像也不报错, 可以看一下具体的类型

type test = typeof curriedCb
type test1 = typeof r1

assertType<Equals<test, <T extends any[]>(...args: T) => CurryV2<Tail<T>, boolean>>>()
assertType<Equals<test1, <T extends any[]>(...args: T) => boolean>>()

由于any[]的存在, 失去了类型检测. 因此目前来看还需要做更多

递归类型与其他泛型推导

根据之前的实践发现, 现在需要一些工具类型来帮助追踪已经传入的参数, 也就是说, 没错调用之后需要明确知道有哪些参数已经被传入进去(消耗掉的), 还有哪些参数等待被传入. 接下来的工具函数都是为了帮助做到这点

Last

定义: 返回元祖类型的最后一个元素

type Last<T extends any[]> = { 0: Last<Tail<T>>, 1: Head<T> }[HasTail<T> extends true ? 0 : 1]

简单讲一讲推断思路: 最后返回的是 {}[], 也就是该对象的结果, 其中该对象的 key 需要判断, 判断条件是 T 中元素是否> 2, 是继续递归调用, 同时 T 使用 Tail<T> 进行数量缩减, 否则返回最后的元素

伪代码为:

const Last = (T) => {
  const o = {
    0: Last<Tail<T>>,
    1: Head<T>
  }
  const k = HasTail<T> === true ? 0 : 1
  return o[k]
}

测试一下:

Last<[1, 2, 3, 4]> // 4

Length

定义: 返回一个元祖类型的长度

type Length<T extends any[]> = T['length']

测试一下:

Length<[]> // 0
Length<any, any> // 2

Prepend

定义: 给定一个类型和一个元祖类型, 在该元祖类型的头部插入这个类型, 输出该新元祖类型

type Prepend<E, T extends any[]> = 
  ((arg: E, ...args: T) => any) extends ((...args: infer U) => any) ? U : T

测试一下

Prepend<string, []> // [string]
Prepend<number, [1, 2]> // [number, 1, 2]

Drop

定义: 给定一个数字 N, 一个元祖类型, 返回从索引从数字之后(包括)的所有类型

type Drop<N extends number, T extends any[], I extends any[]=[]> = {
  0: Drop<N, Tail<T>, Prepend<any, I>>,
  1: T
}[Length<I> extends N ? 1 : 0]

简单讲一讲推导思路: 给定一个 I 为空元祖, 每次 Drop 一次就往里面添加一个元素, 不断比较 NI 的长度, 如果 I 的长度 < N 继续递归调用, 否则返回最终结果

测试一下:

Drop<2, [0, 1, 2, 3]> // [2, 3]

Cast

定义: 检查 X 是否是 Y 类型, 如果满足保持 X 类型, 否则返回 Y 类型

type Cast<X, Y> = X extends Y ? X : Y

测试一下:

Cast<string, any> // string
Cast<string, string | number> // string
Cast<string, number> // number

柯里化类型 2

讲一讲如何追踪已经传入的参数的思路, 假设有如下两个类型parametersconsumed, 分别代表总的(一开始)需要的参数元祖类型和已经传入(consumed)的参数元祖类型:

type parameters = [string, number, boolean, string[]]
type consumed = [string, number]

配合之前的工具类型可以轻松得到剩余需要的类型列表

type toConsume = Drop<Length<consumed>, parameters> // [boolean, string[]]

CurryV3

根据上面的思路可以实现第三版:

type CurryV3<P extends any[], R> =
  <T extends any[]>(...args: T) =>
    Length<Drop<Length<T>, P>> extends 0
      ? R : CurryV3<Drop<Length<T>, P>, R>

简单讲一下推导思路: 核心在于Drop<Length<T>, P>, 这个类型代表的就是剩余需要被传入的参数, 利用Length进行判断是否还存在参数等待被传入

然而实现的时候会存在错误:

Type instantiation is excessively deep and possibly infinite.

这时需要使用之前提到的Cast帮助修复

CurryV4

type CurryV4<P extends any[], R> =
  <T extends any[]>(...args: Cast<T, Partial<P>>) =>
    Length<Drop<Length<T>, P> extends infer DT ? Cast<DT, any[]> : never> extends 0
      ? R
      : CurryV4<Drop<Length<T>, P> extends infer DT ? Cast<DT, any[]> : never, R>      

测试一下:

declare function curryV4<P extends any[], R>(f: (...args: P) => R): CurryV4<P, R>

const curriedCb = curryV4(curryCb1)

const r1 = curriedCb('name') // ok
const r2 = curriedCb('name', 1, true) // ok

似乎对于严格的单个参数柯里化调用和非严格的函数调用都没有问题. 然而, 假如尝试加入...rest参数

const curriedCb2 = curryV4(curryCb2)
const r3 = curriedCb2('name', 1)('f')('a', 'aaa') // error

出现了两个问题:

  • curriedCb2('name', 1)('f')('a', 'aaa')这样的调用方式其实是错误的, 正确方式应该是curriedCb2('name', 1, 'f', 'a', 'aaa')也就是...rest参数应该是随着最后一个参数一起传入完毕, 不应该返回出一个新的函数类型来接受...rest参数
  • 永远也拿不到最后的boolean结果了

原因其实是这样, Length类型是无法推导出...rest的参数个数的, 毕竟他无法确切知道你需要传入的参数到底是有几个

type restargs = [string, number, boolean, ...string[]]
type restargsLength = Length<restargs> // number

推导结果是number....其实是一个挺不错的结果了

CurryV5

type CurryV5<P extends any[], R> =
  <T extends any[]>(...args: Cast<T, Partial<P>>) =>
    Drop<Length<T>, P> extends [any, ...any[]]
      ? CurryV5<Drop<Length<T>, P> extends infer DT ? Cast<DT, any[]> : never, R>
      : R

这里使用[any, ...any[]]来进行判断是否是最后一次非...rest参数的传入, 同时配合Cast保证返回正确类型

测试一下:

const curriedCb = curryV5(curryCb1)
const curriedCb2 = curryV5(curryCb2)

// 单个参数调用
const r1 = curriedCb('name')
// 多个参数调用
const r2 = curriedCb('name', 1, true)
// ...rest 参数调用
const r3 = curriedCb2('name', 1, 'aaa')

算是基本实现了功能

占位符(Placeholders)

个人不太熟悉Ramda, 不做深究, 不过lodash源码里面也提到了一些, 这里仅做一个简单了解

下面几种调用是等价的:

f(1, 2, 3)
f(_, 2, 3)(1)
f(_, _, 3)(1)(2)
f(_, 2, _)(1, 3)
f(_, 2)(1)(3)
f(_, 2)(1, 3)
f(_, 2)(_, 3)(1)

这里的_就是作为一个占位符, 即 placeholder, 或叫作 gap

这块有兴趣还是建议去看作者原文

关于原文和源码

作者最后提到, 这篇文章其实是ts-toolbelt这个 repo 的入门教程, 这个 repo 涵盖了很多高级实用的 typescript 类型定义, 欢迎 star

以及, 作者将源码也放到了github上, 增加了一些新的内容, 比如pipe等新的类型定义, 可以自己 clone 下来试一试

再次感谢作者@pirix-gh

参考

@hacker0limbo hacker0limbo added the typescript typescript 笔记整理 label Apr 12, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
typescript typescript 笔记整理
Projects
None yet
Development

No branches or pull requests

1 participant