为什么 TypeScript 对条件函数类型和仅具有条件返回类型的相同函数类型的处理方式不同?

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

我有两种函数类型,我希望它们是等效的,但它们的行为不同。

这些是类型:

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);
    //    ^?
}
typescript
1个回答
0
投票

对于任何特定

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

使用 

Extract

 实用程序类型说服编译器允许 
Ret<T, M>(如果
M
确实可分配给
TyMap<T>
,则
Extract<M, TyMap<T>>
将是
M
。如果不是,则可能是
 never
)。
但是这些担忧超出了所提出问题的范围,所以我不会再离题了。

Playground 代码链接

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