我有两种函数类型,我希望它们是等效的,但它们的行为不同。
这些是类型:
type MatchT<T extends {id: string}, M> =
[M] extends [TyMap<infer X>]
? [T] extends [X]
? (t: T, m: M) => Ret<T, M>
: (t: T, m: M) => never
: (t: T, m: M) => never;
和:
type MatchT<T extends {id: string}, M> = (t: T, m: M) =>
[M] extends [TyMap<infer X>]
? [T] extends [X]
? Ret<T, M>
: never
: never;
区别在于,在第一种情况下,整个函数类型都在条件内部,而在第二种情况下,只有返回类型。
MatchT
用于将类型为 <T>(t: T, map: TyMap<T>): Ret<T, TyMap<T>>
的函数重新键入为类型为 <T, X>(t: T, map: TyMap<X>): Ret<T, TyMap<X>>
的函数,只要 T
是 X
的子类型即可。 (这些类型在 TS 中无效,但它们很短并且可以很好地直观地了解我想要做什么)。never
。通过实验,我注意到 [M] extends [TyMap<infer X>]
的计算结果为 false
,但仅限于 MatchT
的第二个版本。
为什么这两种类型给我不同的结果?
您可以在 Playground 上玩一个完整的工作示例:https://tsplay.dev/NVX7MW。命名空间
test_1
使用我在此处发布的 MatchT
的第一个版本,而 test_2
使用第二个版本。否则,您可以在这里找到示例代码:
type A = {id: "A", a: number}
type B = {id: "B", b: string}
type TyMap<T extends {id: string}> = {
[K in T['id']]: (x: T & {id: K})=>unknown
}
type Ret<T extends {id: string}, M extends TyMap<T>> = ReturnType<M[T['id']]>;
function match<T extends {id: string}, M extends TyMap<T>>(t: T, map: M): Ret<T, M> {
const f = (map as Record<string,unknown>)[t.id] as (x: T) => Ret<T, M>;
return f(t);
}
declare const a: A, ab: A | B;
namespace test_1 {
const matchT = <T extends {id: string}, M>(t: T, m: M) => {
return (match as MatchT<T, M>)(t, m);
}
type MatchT<T extends {id: string}, M> =
[M] extends [TyMap<infer X>]
? [T] extends [X]
? (t: T, m: M) => Ret<T, M>
: (t: T, m: M) => never
: (t: T, m: M) => never;
const wrap = <T extends A|B>(t: T) => {
return matchT(t, {A: a => a.a, B: b => b.b} satisfies TyMap<A|B>);
}
const a1 = wrap(a);
// ^?
const ab1 = wrap(ab);
// ^?
}
namespace test_2 {
const matchT = <T extends {id: string}, M>(t: T, m: M) => {
return (match as MatchT<T, M>)(t, m);
}
type MatchT<T extends {id: string}, M> = (t: T, m: M) =>
[M] extends [TyMap<infer X>]
? [T] extends [X]
? Ret<T, M>
: never
: never;
const wrap = <T extends A|B>(t: T) => {
return matchT(t, {A: a => a.a, B: b => b.b} satisfies TyMap<A|B>);
}
const a1 = wrap(a);
// ^?
const ab1 = wrap(ab);
// ^?
}
对于任何特定
T
和 M
,您的两个版本的 MatchT<T, M>
将评估为相同类型。但是,当 T
和 M
是 generic 时,可能会发生不同的事情,因为在不同阶段,TypeScript 需要决定是否defer 对具有泛型的类型求值,保持 T
或 M
原样(这始终是“正确的”,但这意味着该类型可能是无用的不透明斑点)或者是否尝试根据一些启发式来猜测它将评估什么,例如将T
和M
扩大到它们的约束(它为您提供了一些更具体的类型来使用,但并不总是准确的)。
在这两种情况下,您都有一个类型为
match
的函数 Match<T, M>
,并且分别使用类型为 T
和 M
的参数来调用它。当Match<T, M>
定义为时
type MatchT<T extends { id: string }, M> = (t: T, m: M) =>
[M] extends [TyMap<infer X>]
? [T] extends [X]
? Ret<T, M>
: never
: never;
那么这看起来就像一个单一函数类型,其参数的类型为
T
和 M
。该函数可直接调用,返回类型为 [M] extends [TyMap<infer X>] ? [T] extends [X] ? Ret<T, M> : never : never;
编译器无需尝试计算 T
和 M
即可产生此结果。这种条件类型保持“延迟”状态。这很可能是正确的(如果您不喜欢这种行为,那是因为您的类型在某种程度上不正确,而不是因为函数调用的行为不正确。)
另一方面,当 Match<T, M>
定义为
type MatchT<T extends { id: string }, M> =
[M] extends [TyMap<infer X>]
? [T] extends [X]
? (t: T, m: M) => Ret<T, M>
: (t: T, m: M) => never
: (t: T, m: M) => never;
那么这看起来像是您正在尝试调用不是单一函数类型的东西。为了继续,编译器需要对
T
和
M
进行一些猜测。这里的细节没有很好地记录,但是你得到的看起来像 (t: T, m: M) => Ret<T, M>
(在我的研究中你得到类似 ((...args: [t: T, m: M] | [t: T, m: M]) => Ret<T, M> | never)
的东西,这相当于那个)。所以无论如何,函数返回类型只是Ret<T, M>
。它不再以 T
和 M
为条件。这并不能准确地表示运行时实际发生的情况;这是一种简化。但如果没有这样的简化,TypeScript 只能耸耸肩说 match
将完全无法调用。
这就是行为差异的原因。我想说你永远不想调用通用条件函数。如果可以避免的话,您也不想编写自己的复杂条件类型。我可能更喜欢 MatchT
的形式
type MatchT<T extends { id: string }, M> = (t: T, m: M) =>
Ret<T, Extract<M, TyMap<T>>>;
使用
实用程序类型说服编译器允许
Ret<T, M>
(如果 M
确实可分配给 TyMap<T>
,则 Extract<M, TyMap<T>>
将是 M
。如果不是,则可能是 never
)。但是这些担忧超出了所提出问题的范围,所以我不会再离题了。
Playground 代码链接