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

问题描述 投票:0回答:1

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

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

typescript
1个回答
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,请给我T[EverythingLessThan<K>]并进行操作。没有简单的方法可以表示EverythingLessThan<K>,因此我制作了一个名为LT的元组,并为K"8"的值进行了硬编码。可以根据需要扩展它,也可以用一些尚不支持的聪明的递归类型替换它,只要它们都不使它接近我负责的生产代码。

ScanFuncs类型别名通过将每个函数T与另一个返回类型未更改但参数类型为该函数的交集的函数进行比较,将one-arg函数类型的元组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}) =>的回调,因为后者将导致隐式any类型。由于design limitation我已将mentioned to you before排除掉,因此您对编译器从先前参数的返回值中获取参数类型的任何希望都应被剔除。


这两个推理问题以及元组简化操作的混乱都向我强烈暗示,在TypeScript中完成此操作的惯用方式将是使用构建器模式。它可能看起来像这样:

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)); }); } }

这可能不是

best

的实现,但是您可以看到键入的疯狂程度大大降低了。 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();

请注意,类型推断现在可以使用,您不必在z调用中注释ythen()。您仍然必须在初始x参数中注释ynew Coalesce(),但这是有道理的,因为编译器无处可推断。它的行为相同:

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" }

看起来不错!


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

Link to code

© www.soinside.com 2019 - 2024. All rights reserved.