我有两种功能类型,我希望它们是等效的,但它们的行为不同.

这些是类型:

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 is used to re-type a function with type <T>(t: T, map: TyMap<T>): Ret<T, TyMap<T>> into one with type <T, X>(t: T, map: TyMap<X>): Ret<T, TyMap<X>> as long as T is a subtype of X. (These types aren't valid in TS, but they're short and give a good intuition on what I'm trying to do).
The first version works as I'd expect, while the second one fails and returns never. Experimentally I noticed that [M] extends [TyMap<infer X>] evaluates to false, but only in the second version of MatchT.

为什么这两种类型给我的结果不同?

您可以在操场上玩一个完全可行的示例: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);
    //    ^?
}

推荐答案

对于任何specific TM,您的MatchT<T, M>的两个版本将判断为相同类型. 但当TM等于generic时,可能会发生不同的事情,因为在各个阶段,TypScript需要决定是否对保持TM的通用类型进行defer判断(这总是"正确的",但这意味着该类型可能是无用的不透明斑点)或者是否try 根据一些启发式方法将其判断结果计算为guess,例如将TM扩展到它们的constraints(这为您提供了一些更具体的类型来使用,但并不总是准确的).


在这两种情况下,您都有一个类型为Match<T, M>的函数match,并且分别用类型为TM的参数来调用它. 当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;

那么这看起来就像一个参数为类型TM的单个函数类型.该函数可直接调用,返回类型为[M] extends [TyMap<infer X>] ? [T] extends [X] ? Ret<T, M> : never : never;,编译器无需try 计算TM即可产生此结果.此条件类型保持deferred.这很可能是正确的(如果您不喜欢这种行为,那是因为您的类型在某种程度上不正确,而不是因为函数调用的行为不正确.)

另一方面,当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;

那么这看起来就像您正在try 调用不是单个函数类型的东西.为了继续,编译器需要对TM进行一些猜测.这里没有很好地记录详细信息,但你得到的结果看起来像(t: T, m: M) => Ret<T, M>(在我的研究中,你得到的结果像((...args: [t: T, m: M] | [t: T, m: M]) => Ret<T, M> | never),相当于((...args: [t: T, m: M] | [t: T, m: M]) => Ret<T, M> | never)). 因此无论如何,函数返回类型都只是Ret<T, M>. 它不再以TM为条件. 这并不是运行时实际发生的事情的准确表示;这是一种简化.但如果没有这样的简化,TypScript只能耸耸肩,说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 link to code

Typescript相关问答推荐

类型订阅函数以确保回调仅从联合类型中接收正确的事件

您可以创建一个类型来表示打字员吗

TypScript如何在 struct 上定义基元类型?

Angular 17 -如何使用新的@if语法在对象中使用Deliverc值

在类型内部使用泛型类型时,不能使用其他字符串索引

typescribe警告,explate—deps,useEffect依赖项列表

contextPaneTitleText类型的参数不能分配给remoteconfig类型的关键参数'""'''

泛型类型,引用其具有不同类型的属性之一

有没有办法解决这个问题,类型和IntrinsicAttributes类型没有相同的属性?

类型脚本基于数组作为泛型参数展开类型

如何表示多层深度的泛型类型数组?

打印脚本中的动态函数重载

在排版修饰器中推断方法响应类型

在打印脚本中使用泛型扩展抽象类

设置不允许相同类型的多个参数的线性规则

打字脚本错误:`不扩展[Type]`,而不是直接使用`[Type]`

有没有更干净的方法来更新**key of T**的值?

在tabel管理区域react js上设置特定顺序

TypeScript:如何使用泛型和子类型创建泛型默认函数?

Svelte+EsBuild+Deno-未捕获类型错误:无法读取未定义的属性(读取';$$';)