【问题标题】:TypeScript generic conditional type as return value for generic functionTypeScript 泛型条件类型作为泛型函数的返回值
【发布时间】:2022-01-02 11:29:02
【问题描述】:

背景

作为我正在研究的数据分析库的一部分,我正在创建一组函数,用于从字符串中读取某些类型的值。这个想法本质上是定义一个 CSV 文件的结构,所以我可以将它的每个单元格读取为一个字符串,并且知道它应该是什么类型,然后将该字符串转换为适当类型的值。

一些单元格包含多个值,并被转换为数组。其他单元格包含单个值。

我在这里所做的部分工作是删除与预期不符的值,并记录警告。对于包含数组的单元格,我很高兴结果仍然是一个数组,即使它是空的。但是对于包含单个值的单元格,我想将无效值转换为null

为此,我设置了一个泛型类型,供所有这些“转换器”函数共享,它返回符合这些要求的条件类型:

type TransformerFn<T> = (value: string, locationIdentifier?: string) => T extends any[] ? T : (T | null);

到目前为止,在我实现的简单案例中,例如将字符串拆分为字符串数组或提取布尔值,这工作得很好。 TypeScript 在解析 TransformerFn&lt;string[]&gt;TransformerFn&lt;boolean&gt; 的条件时没​​有问题。

但我拥有的其中一个转换器本质上是用于确认列中的每个单元格要么是空的,要么包含字符串 enum 中的值,而这里我遇到了一个问题。

当我将它用作函数参数时,我一直在将该字符串枚举键入为Record&lt;string, string&gt;,它一直运行良好。但是,最近我添加了一些功能来重新编码显然应该是特定枚举值但尚未正确加载的数据。

为了实现这一点,我使用了一个带有约束的泛型类型来表示字符串枚举值的联合:

export function enumValue<E extends string>(enums: Record<string, E>, recodeMap?: Record<string, E>)

这一直很好,直到我尝试添加我之前提到的 TransformerFn 输入。

问题

尽管我的泛型类型 E 有一个约束,即 extends string,这意味着 E extends any[] 永远不会为真,TypeScript 还是无法解析我的泛型条件类型。

这是它给我的错误:

类型'(值:字符串,位置标识符?:字符串|未定义)=> E | null' 不能分配给类型 'TransformerFn'。 'E 型 | null' 不能分配给类型 'E extends any[] ? E : E |无效的'。 类型“null”不能分配给类型“E extends any[]? E : E |空'。

这是我的代码:

type TransformerFn<T> = (value: string, locationIdentifier?: string) => T extends any[] ? T : (T | null);

/**
 * Checks that the value, if it exists, is a member of an enum.
 *
 * If the value does not exist, it is transformed to null.
 *
 * If a recoding map is passed, and it contains instructions for this value, it is recoded first.
 *
 * If the value exists but it is not a member of the enum and cannot be recoded,
 * a warning will be generated and null will be returned.
 */
export function enumValue<E extends string>(enums: Record<string, E>, recodeMap?: Record<string, E>): TransformerFn<E> {
    const enumValues: E[] = Object.values(enums);

    function isEnumMember(val: unknown): val is E {
        return (enumValues as any[]).includes(val);
    }

    const transformer: TransformerFn<E> = (value: string, locationIdentifier?: string) => {
        if (!value) {
            return null;
        }

        if (isEnumMember(value)) {
            return value;
        }

        if (recodeMap && value in recodeMap) {
            const recodedValue = recodeMap[value];
            return recodedValue;
        }

        console.warn(`Value '${value}' does not exist within ${enumValues.join(', ')} (${locationIdentifier})`);
        return null;
    };

    return transformer;
}

TypeScript Playground

最小可重现示例

type TransformerFn<T> = () => T extends any[] ? T : null;

function enumValue<E extends string>(): TransformerFn<E> {
    const transformer: TransformerFn<E> = () => {
        return null;
    };

    return transformer;
}

TypeScript Playground

类型“() => null”不可分配给类型“TransformerFn”。 类型“null”不能分配给类型“E extends any[]? E : null'。


现在我当然可以放弃并为我的enumValue 函数的返回值使用非条件类型,因为我知道它应该是E | null,而不是尝试使用我的TransformerFn 条件类型。我的代码仍然可以正常工作,实际上不会对我造成任何维护问题。

但是我遇到了一些我不理解并且无法弄清楚的事情。那么,谁能向我解释为什么这不起作用,如果有什么我可以做的事情会起作用吗?

【问题讨论】:

    标签: typescript typescript-generics conditional-types


    【解决方案1】:

    这是 TypeScript 当前的限制或缺失的功能;编译器通常推迟conditional type 的评估,这取决于未解析/未指定的 generic 类型参数,例如在泛型函数的实现内部。这通常是您可以做的最好的事情,因为编译器通常无法知道条件类型将是什么,直到它确切知道检查的类型是什么。但是:在某些情况下,泛型类型参数是constrained,这样编译器应该能够更早地评估条件类型。

    例如,如果您有一个受约束的泛型类型参数T extends A,那么即使T 并不确切知道,类似T extends A ? X : Y 的任何东西都可以缩小为X。或者,如果有另一种类型 B 使得 A &amp; Bnever,那么即使 T 不确切知道,T extends B ? X : Y 也可以缩小为 Y

    您的示例情况是后者:在E extends string 的情况下,条件类型E extends any[] ? E : null; 应该评估为null,因为string &amp; any[] 类型基本上是不可能的(实际上并不那么简单;类型如string &amp; any[] 不会被编译器简化为 never,因此有可能出现 null 以外的其他内容,但让我们忽略这里的皱纹)。

    无论如何,这种对泛型条件类型的早期评估不会发生。 microsoft/TypeScript#23132 建议使用泛型约束来评估条件类型。该问题仍被标记为“等待反馈”,因此,如果您认为您的用例特别引人注目(并且该问题中尚未提及),那么您可能想去那里进行描述。无论如何,你可以给它一个?。但实际上,这个问题已经公开了很长时间,并且没有迹象表明它会很快或永远实施。


    目前,您只有解决方法。在这种情况下,最好的方法是使用type assertion,它专门用于开发人员对表达式类型的了解多于编译器无法计算的情况。如果您确定transformer 将是一个有效的TrandformerFn&lt;E&gt;,那么您可以告诉编译器:

    const transformer = (() => {
        return null;
    }) as TransformerFn<E>; // no error
    

    当然,这意味着您要从编译器那里获得维护该行的类型安全的工作,因此您应该小心自己是否正确地完成了这项工作,因为编译器无法分辨出一种或另一种方式:

    const badTransformer = (() => {
        return "oopsie";
    }) as TransformerFn<E>; // also no error
    

    Playground link to code

    【讨论】:

      【解决方案2】:

      我的泛型类型E 有一个约束,它扩展string,这意味着E extends any[] 永远不会为真

      就 Typescript 而言并非如此。 string &amp; any[]不是never,但即使是,never也是E允许的类型; never 是所有其他类型的子类型,因此它确实满足约束,而且never extends any[] 也是 true。

      请注意,由于分布条件类型如何与never 一起工作的怪癖(请参阅here),您的类型TransformerFn&lt;never&gt; 实际上是() =&gt; never,这意味着一个永远不会返回的函数(即它抛出异常或它不会终止)。你的返回null的函数不能分配给这个类型,所以错误信息是正确的。

      【讨论】:

        猜你喜欢
        • 2019-10-13
        • 2021-12-06
        • 1970-01-01
        • 2021-10-06
        • 2020-11-10
        • 1970-01-01
        • 1970-01-01
        • 2018-07-03
        • 1970-01-01
        相关资源
        最近更新 更多