[英]Chain functions and infer types from previous return
是否可以从前一个链中声明的相应键中键入以下每个函数的 arguments?
const helloWorld = example(
({}) => ({ greeting: 'Hello', name: 'Thomas' }),
({ greeting, name }) => ({ clean: `${greeting} ${name}` }),
({ clean }) => ({ cleanLength: clean.length }),
({ name }) => ({ nameLength: name.length }),
)
我从这里有这个CheckFuncs
泛型,它检查每个 function 的类型是否相互对齐。
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 }
}
这相当丑陋,我认为我没有精力解释它。 这已经够混乱了,我强烈建议放弃任何需要对元组类型进行“减少”操作的东西,转而使用构建器。 我将首先展示我正在做的事情的草图。 这是代码:
// 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)
}
从根本上说,您正在检查元组中的每个 function 与所有前面的函数,因此您需要类似:对于元组T
中的给定索引键K
,给我T[EverythingLessThan<K>]
并操作它。 没有简单的方法来表示EverythingLessThan<K>
,所以我创建了一个名为LT
的元组,其中K
的硬编码值最高为"8"
。 它可以根据需要进行扩展,或者用一些聪明但不受支持的递归类型替换,只要它们都不会接近我负责的生产代码。
The ScanFuncs
type alias converts a tuple T
of one-arg function types into a compatible type, by comparing each function T[K]
to another function whose return type is unchanged, but whose parameter type is the intersection of the first function's parameter type and所有先前的函数都返回类型'。 我在那里使用UnionToIntersection
,如果您的功能涉及工会本身,这可能会做一些奇怪的事情。 它可以被防范,但更复杂,所以我不打扰。
我实现了您的example
,我将其称为coalesce
是因为需要一个更具启发性的名称,作为 function ,它采用T
类型的单参数回调元组,使用ScanFuncs<T>
检查它,并返回一个单参数 function 其参数type 是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
类型。 由于我之前向您提到的设计限制,您希望让编译器从先前 arguments 的返回值推断参数类型的任何希望都应该被扼杀。
这个推理问题和元组归约操作的混乱都强烈地向我暗示,在 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));
});
}
}
这可能不是最好的实现,但您可以看到打字不那么疯狂。 CollapseIntersection
确实是其中唯一“奇怪”的东西,这只是为了让像{x: 1, y: 2} & {z: 3} & {w: 4}
这样的笨拙类型更容易作为{x: 1, y: 2, z: 3, w: 4}
。
构建器通过将后续then()
函数折叠到其当前回调中来工作,并仅跟踪当前 output 类型和整体输入类型。
你像这样使用它:
const f = new Coalesce(
({ x, y }: { x: number, y: string }) => ({ z: y.length === x })
).then(
({ z }) => ({ v: !z })
).then(
({ y }) => ({ w: y.toUpperCase() })
).build();
请注意,类型推断现在有效,您不必在then()
调用中注释z
或y
。 您仍然必须在初始new Coalesce()
参数中注释x
和y
,但这是有道理的,因为编译器无处可推断它。 它的行为相同:
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" }
看起来不错!
好的,希望有帮助; 祝你好运!
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.