链函数和先前返回的推断类型

时间:2019-11-20 01:36:36

标签: typescript

是否可以从上一个链中声明的相应键中键入以下每个函数的参数?

const helloWorld = example(
    ({}) => ({ greeting: 'Hello', name: 'Thomas' }),
    ({ greeting, name }) => ({ clean: `${greeting} ${name}` }),
    ({ clean }) => ({ cleanLength: clean.length }),
    ({ name }) => ({ nameLength: name.length }),
)

我有这个CheckFuncs泛型from here,它检查每个功能的类型是否彼此对齐。

type Tail<T extends readonly any[]> =
    ((...a: T) => void) extends ((h: any, ...r: infer R) => void) ? R : never;

type CheckFuncs<T extends readonly ((x: any) => any)[]> = { [K in keyof T]:
    K extends keyof Tail<T> ? (
        [T[K], Tail<T>[K]] extends [(x: infer A) => infer R, (x: infer S) => any] ? (
            [R] extends [S] ? T[K] : (x: A) => S
        ) : never
    ) : T[K]
}

function journey<T extends readonly ((x: any) => any)[]>(...t: CheckFuncs<T>) {

}

这有点不同。

在这里,我们可以假设一些事情:

  • 每个函数只能接受一个{}参数
  • 每个函数必须返回{}
  • 在使用或传递给创建的函数之前,必须在链中声明该参数。

理想的语法:

const helloWorld = example(
    ({ greeting, name }) => ({ clean: `${greeting} ${name}` }),
    ({ clean }) => ({ cleanLength: clean.length }),
    ({ name }) => ({ nameLength: name.length }),
)

helloWorld({ greeting: 'Hello', name: 'Thomas' })

这可能吗?


也对密钥版本感兴趣:

const helloWorld = mac({
    clean: ({ greeting, name }) => `${greeting} ${name}`,
    cleanLength: ({ clean }) => clean.length,
    nameLength: ({ name }) => name.length,
})

这是尝试,但是:

  

'a'在其自己的类型注释中直接或间接引用。

const JourneyKeyed = <T extends JourneyKeyed.Objects<T>>(a: T) => {
    return a
}

const helloWorld = JourneyKeyed({
    greeting: ({ name }) => name === 'bob' ? 'Get out!' : 'Welcome',
    fullGreeting: ({ greeting, name }) => `${greeting} ${name}`,
})


namespace JourneyKeyed {
    type ThenArg<T> = T extends Promise<infer U> ? U : T
    type FirstArg<T extends any> =
        T extends [infer R, ...any[]] ? R :
        T extends [] ? undefined :
        T;
    type KeyedReturns<C, M extends Array<keyof C>> = {
        [K in M[number]]: C[K] extends ((...args: any[]) => any) ? FirstArg<ThenArg<ReturnType<C[K]>>> : never
    }
    type AllKeyedReturns<T> = KeyedReturns<typeof helloWorld, Array<keyof typeof helloWorld>>
    export type Objects<T extends object> = { [K in keyof T]: (a: AllKeyedReturns<T[K]>) => T[K] extends Func ? ReturnType<T[K]> : never }
}

Playground

1 个答案:

答案 0 :(得分:1)

这很丑陋,我认为我没有能力解释它。这很凌乱,我强烈建议放弃任何需要对元组类型执行“ reduce”运算的操作,而改为使用builder。我只展示我先做的草图。这是代码:

// extend as needed I guess
type LT = [never, 0, 0 | 1, 0 | 1 | 2, 0 | 1 | 2 | 3, 0 | 1 | 2 | 3 | 4,
  0 | 1 | 2 | 3 | 4 | 5, 0 | 1 | 2 | 3 | 4 | 5 | 6, 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
];
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type UI<U> = UnionToIntersection<U> extends infer O ? { [K in keyof O]: O[K] } : never;
type P0<T> = T extends (x: infer A) => any ? A : never;
type Ret<T> = T extends (x: any) => infer R ? R : never;
type Idx<T, K, D = never> = K extends keyof T ? T[K] : D;

type ScanFuncs<T extends readonly ((x: any) => any)[]> = {
  [K in keyof T]: (x: UI<ReturnType<Idx<T, Idx<LT, K>>> | P0<T[0]>>) => Ret<T[K]>
}

function coalesce<T extends readonly ((x: any) => any)[]>(
  ...args: T & ScanFuncs<T>
): (x: P0<T[0]>) => UI<ReturnType<T[number]> | P0<T[0]>>;
function coalesce(...args: readonly ((x: any) => any)[]) {
  return (x: any) => args.reduce((a, s) => Object.assign(a, s(a)), x)
}

从根本上来说,您正在对元组中的每个函数与之前的所有功能进行检查,因此您需要执行以下操作:对于元组K中给定的索引键T,请给我{{1}并进行操作。没有简单的方法来表示T[EverythingLessThan<K>],所以我制作了一个名为EverythingLessThan<K>的元组,其中包含LTK的硬编码值。可以根据需要对其进行扩展,或者替换为一些尚不支持的聪明的递归类型,只要它们都不使它接近我负责的生产代码即可。

"8"类型别名通过将每个函数ScanFuncs与返回类型不变但其返回类型不变的另一个函数进行比较,将one-arg函数类型的元组T转换为兼容类型。参数类型是第一个函数的参数类型与所有先前函数的返回类型的交集。我在其中使用T[K],如果您的函数涉及联合本身,这可能会做奇怪的事情。可以防范它,但是更复杂,所以我不会打扰。

我实现了您的UnionToIntersection,我将其命名为example是为了获得一个更受启发的名称,它是一个函数,它接受类型为coalesce的单参回调元组,并对其进行检查与T一起使用,并返回一个参数参数为ScanFuncs<T>且返回类型为T[0]与所有T[0]的交集的单参数函数。让我们演示一下它的工作原理:

T

看起来不错。

请注意,您几乎必须注释诸如const f = coalesce( ({ x, y }: { x: number, y: string }) => ({ z: y.length === x }), ({ z }: { z: boolean }) => ({ v: !z }), ({ y }: { y: string }) => ({ w: y.toUpperCase() }) ) const r = f({ x: 9, y: "newspaper" }) /* const r: { x: number; y: string; z: boolean; v: boolean; w: string; } */ console.log(r); // { x: 9, y: "newspaper", z: true, v: false, w: "NEWSPAPER" } 之类的回调,而不是({x, y}: {x: number, y: string}) =>,因为后者将导致隐式({x, y}) =>类型。由于design limitation我已经mentioned to you before,您希望编译器推断从先前参数的返回值中提取参数类型的任何希望。


这个推断问题和tuple-reduce操作的混乱都向我强烈暗示,在TypeScript中完成此操作的惯用方法将是使用构建器模式。看起来可能像这样:

any

那可能不是 best 的实现,但是您可以看到键入的疯狂程度大大降低了。 type CollapseIntersection<T> = Extract<T extends infer U ? { [K in keyof U]: U[K] } : never, T> class Coalesce<I extends object, O extends object> { cb: (x: I) => (I & O) constructor(cb: (x: I) => O) { this.cb = x => Object.assign({}, x, cb(x)); } build() { return this.cb as (x: I) => CollapseIntersection<I & O> } then<T>(cb: (x: I & O) => T) { return new Coalesce<I, O & T>(x => { const io = this.cb(x); return Object.assign(io, cb(io)); }); } } 实际上是其中唯一的“怪异”事物,这仅仅是为了使像CollapseIntersection这样的笨拙的类型更易于与{x: 1, y: 2} & {z: 3} & {w: 4}一起使用。

构建器的工作方式是将后续的{x: 1, y: 2, z: 3, w: 4}函数折叠到其当前回调中,并仅跟踪当前输出类型和整体输入类型。

您可以这样使用它:

then()

请注意,类型推断现在可以使用,您不必在const f = new Coalesce( ({ x, y }: { x: number, y: string }) => ({ z: y.length === x }) ).then( ({ z }) => ({ v: !z }) ).then( ({ y }) => ({ w: y.toUpperCase() }) ).build(); 调用中注释zy。您仍然必须在初始then()参数中注释xy,但这是有道理的,因为编译器无处可推断。它的行为相同:

new Coalesce()

看起来不错!


好的,希望能有所帮助;祝好运!

Link to code