【问题标题】:Type of class-field based on generic基于泛型的类字段类型
【发布时间】:2020-05-11 15:04:38
【问题描述】:

想象以下代码 (playground):

type AvailableTypes = {
    'array': Array<any>;
    'string': string;
    'object': object;
}

class Wrapper<T extends keyof AvailableTypes> {

    // Is either array, string or object
    private readonly type: T;

    // ERROR: Property 'value' has no initializer and is not definitely assigned in the constructor.
    private readonly value: AvailableTypes[T];

    constructor(type: T) {
        this.type = type;

        /**
         * ERROR:
         *  TS2322: Type 'never[]' is not assignable to type 'AvailableTypes[T]'.   
         *  Type 'never[]' is not assignable to type 'never'.
         */
        switch (type) {
            case 'array':
                this.value = [];
                break;
            case 'string':
                this.value = '';
                break;
            case 'object':
                this.value = {};
                break;
        }
    }
}

主要有两个错误:

TS2322:类型“never[]”不可分配给类型“AvailableTypes[T]”。
类型 'never[]' 不能分配给类型 'never'

即使AvailableTypes[T] 总是解析为AvailableTypes 中声明的类型之一,T 是它的关键。

...和

属性“值”没有初始化器,也没有在构造函数中明确赋值。

虽然type 是强制性的,并且必须是stringarrayobject.

我在这里错过了什么?

可能相关的 SO Threads:

更新

(更新为@jcalz answer) 应该可以根据type 属性对value 进行类型检查:

// In the Wrapper class, should work since value can only be an array if type is 'array':
public pushValue(val: unknown) {
    if (this.type === 'array') {
        this.value.push(val);
    }
}

Playground

【问题讨论】:

    标签: typescript generics union-types


    【解决方案1】:

    根本问题是泛型类型参数没有通过控制流分析来缩小范围,这在 TypeScript 中是一个相当长期的未解决问题,请参阅microsoft/TypeScript#24085 了解更多信息。

    当您在switch/case 语句中检查type 时,编译器可以将type 变量的类型缩小为文字类型"array",但它不会缩小类型参数 T"array"。因此它无法验证将any[] 值分配给AvailableTypes[T] 类型是否安全。编译器必须执行一些它目前不执行的分析,例如“好吧,如果type === "array",我们从type 的类型推断T,那么在这个case 块内我们可以缩小@987654340 @ 到"array",因此this.value 的类型是AvailableTypes["array"],又名any[],因此将[] 分配给它是安全的。”但这不会发生。

    同样的问题导致“value 未明确分配”错误。编译器没有足够的资金来查看switch/case 耗尽了T 的所有可能性,因为它不在这里进行控制流分析。


    这里最简单的解决方法是使用type assertions 告诉编译器您知道自己在做什么,因为它无法验证它。

    要处理详尽问题,您可以创建一个 default 抛出的案例,如下所示:

    class Wrapper<T extends keyof AvailableTypes> {
    
        private readonly type: T;
        private readonly value: AvailableTypes[T];
        constructor(type: T) {
            this.type = type;
    
            switch (type) {
                case 'array':
                    this.value = [] as AvailableTypes[T]; // assert
                    break;
                case 'string':
                    this.value = '' as AvailableTypes[T]; // assert
                    break;
                case 'object':
                    this.value = {} as AvailableTypes[T]; // assert
                    break;
                default:
                    throw new Error("HOW DID THIS HAPPEN"); // exhaustive
            }
        }
    }
    

    或者您可以将typeT 扩大到keyof AvailableTypes,这将使编译器执行必要的控制流分析,以了解这些案例是详尽无遗的:

    class Wrapper<T extends keyof AvailableTypes> {
        private readonly type: T;
        private readonly value: AvailableTypes[T];
        constructor(type: T) {
            this.type = type;
            const _type: keyof AvailableTypes = type; // widen to concrete type
            switch (_type) {
                case 'array':
                    this.value = [] as AvailableTypes[T]; // assert
                    break;
                case 'string':
                    this.value = '' as AvailableTypes[T]; // assert
                    break;
                case 'object':
                    this.value = {} as AvailableTypes[T]; // assert
                    break;
            }
        }
    }
    

    另一个解决方法 (mentioned in a comment by person who implemented the soundness change) 是利用以下事实:如果您有一个 t 类型为 T 的值和一个键 k 类型为 K extends keyof T,那么类型为 @987654360 @ 将被编译器视为T[K]。因此,如果我们可以创建一个有效的AvailableTypes,我们就可以使用type 对其进行索引。可能是这样的:

    class Wrapper<T extends keyof AvailableTypes> {
        private readonly type: T
        private readonly value: AvailableTypes[T];
        constructor(type: T) {
            this.type = type;
            const initValues: AvailableTypes = {
                array: [],
                string: "",
                object: {}
            };
            this.value = initValues[type];
        }
    }
    

    这是迄今为止比类型断言和switch 语句更好的方法,并且是相当安全的类型引导。因此,除非您的用例禁止,否则我会采用此解决方案。


    好的,希望其中一个对您有所帮助。祝你好运!

    Playground link to code


    更新:discriminated union instead of generic classes

    【讨论】:

    • 感谢您的详细解答!但是,当我尝试检查 type 是否为 array 并尝试将某些内容推入它失败的值时:(即使 type 基本上严格绑定到 价值属性...
    • 我在问题中看不到这一点;我错过了吗?还是这是一个单独的问题或后续问题? minimal reproducible example 也总是有帮助。祝你好运!
    • 对不起,我已经更新了问题 - type 属性实际上是用于此目的!
    • 问题是一样的,我看到的唯一没有完全重构的解决方案是类型断言:(this.value as AvailableTypes["array"]).push(val);。如果我知道您计划在 Wrapper 的实现中进行此类操作,我建议您改用有区别的联合,或者不使用类或使用一系列子类(联合的每个成员一个子类) .
    • 我在我的答案底部添加了一个游乐场链接,展示了与同级类的有区别的联合如何比单个泛型类更好地工作。祝你好运!
    猜你喜欢
    • 2021-03-26
    • 1970-01-01
    • 2017-11-01
    • 2020-07-04
    • 1970-01-01
    • 2012-09-23
    • 1970-01-01
    • 1970-01-01
    • 2014-03-22
    相关资源
    最近更新 更多