【问题标题】:Unsafe implicit conversion of generics in TypeScriptTypeScript 中泛型的不安全隐式转换
【发布时间】:2018-05-30 17:40:24
【问题描述】:

TypeScript 编译器tsc 编译以下代码没有任何问题,即使带有--strict 标志也是如此。但是,该代码包含一个基本错误,在 Java 或 C# 等语言中是可以避免的。

interface IBox<T> {
  value: T;
}

const numberBox: IBox<number> = { value: 1 };

function insertString(items: IBox<string | number>): void {
  items.value = 'Test';
}

// this call is problematic
insertString(numberBox);

// throws at runtime:
// "TypeError: numberBox.value.toExponential is not a function"
numberBox.value.toExponential();

可以配置tsc 以便识别这样的错误吗?

【问题讨论】:

    标签: typescript generics types type-conversion tsc


    【解决方案1】:

    TypeScript 并没有很好的通用方法来处理contravariance or invariance。粗略地说,如果您正在输出某些内容(函数输出、只读属性),您可以输出比预期类型更窄但不宽的内容(协方差),并且如果您正在输入某些内容(函数输入、只写属性) ) 你可以接受比预期类型更宽但不能更窄的东西(逆变)。如果您正在读取和写入相同的值,则不允许缩小或扩大类型(不变性)。

    这个问题在 Java 中没有出现太多(不确定 C#),主要是因为您不能轻松地创建类型的联合或交集,并且因为在泛型中有 extendssuper 约束起作用作为协方差和逆变的标记。至少在 Java 数组中确实看到了这一点,这些数组被认为是不合理的协变(尝试上面的 Object[]Integer[],你会看到有趣的事情发生)。


    TypeScript 通常在将函数输出视为协变方面做得很好。在 TypeScript v2.6 之前,编译器将函数输入视为 bivariant,这是不合理的(但有一些有用的效果;请阅读链接的常见问题解答条目)。现在有一个 --strictFunctionTypes 编译器标志,可让您对独立函数(而不是方法)的函数输入强制执行逆变。

    目前,TypeScript 将属性值和泛型类型视为协变,这意味着它们适合阅读但不适合书写。这直接导致您看到的问题。请注意,属性值也是如此,因此您可以在没有泛型的情况下重现此问题:

    let numberBox: { value: number } = { value: 1 };
    function insertString(items: { value: string | number }): void {
      items.value = 'Test';
    }
    insertString(numberBox);
    numberBox.value.toExponential();
    

    除了“小心”之外,我没有什么好的建议。 TypeScript 是not intended 拥有严格完善的类型系统(参见非目标#3);相反,语言维护者倾向于只解决导致程序中实际错误的问题。如果这种事情对您影响很大,也许可以前往Microsoft/TypeScript#10717 或类似的问题并给它一个?,或者如果您认为它具有说服力,请描述您的用例。

    希望对您有所帮助。祝你好运!

    【讨论】:

    • 非常感谢! “TypeScript 将属性值和泛型类型视为协变”——直到现在我才注意到从泛型中读取是类型安全的。但是,您的说法似乎并不完全正确。如果泛型包含至少一个协方差相关成员(例如getValue(): T),它似乎只被视为协变。如果T ext. UU ext. T,则允许分配具有至少一个逆变相关成员(例如setValue(a: T))的泛型let x: ISetter&lt;T&gt; = &lt;ISetter&lt;U&gt;&gt; y。一个没有成员的泛型依赖于T,它的类型参数不受限制。
    • 我还发现下面的编译不出来。 let numberBox: { set: (x: number) =&gt; void } = { set: () =&gt; null }; function insertString(items: { set: (x: string | number) =&gt; void }): void { items.set('Test'); } insertString(numberBox); 这个case好像和题中的例子不符。
    • 您的let x: ISetter&lt;T&gt; = &lt;ISetter&lt;U&gt;&gt;y 示例使用了类型断言,这比推断intentionally 更宽松。实际上,类型断言只在不安全的向下转换方向上才需要。
    • “您的let x: ISetter&lt;T&gt; = &lt;ISetter&lt;U&gt;&gt;y 示例使用了类型断言,这比故意推理更宽松”——是的,但我关心的是赋值而不是类型断言。 (由于字符限制,我不得不使用它)。谢谢你把我指向--strictFunctionTypes——见my answer
    【解决方案2】:

    由于 TypeScript 2.6 tsc 有一个命令行选项 --strictFunctionTypes(自动包含在 --strict 中)。如果给定,这将强制对函数类型的参数进行逆变类型检查。出于兼容性原因,方法和构造函数被设计排除在此规则之外。所以要使用逆变,似乎需要使用函数类型:

    interface IBox<T> {
      getValue: () => T;
      setValue: (value: T) => void;
    }
    
    class Box<T> implements IBox<T> {
      private value: T;
      public constructor(value: T) {
        this.value = value;
      }
      public getValue() {
        return this.value;
      }
      public setValue(value: T) {
        this.value = value;
      };
    }
    
    const numberBox: IBox<number> = new Box<number>(1);
    
    function insertString(items: IBox<string | number>): void {
      items.setValue('Test');
    }
    
    // this call does not compile anymore
    insertString(numberBox);
    

    如果在最后 3 个语句中 IBoxBox 替换,则逆变类型检查不适用。因此,为了防止有人意外引用此类类型,Box&lt;T&gt;IBox&lt;T&gt; 可以放在单独的 ES6 模块中,仅公开接口和工厂函数。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-09-12
      • 2018-12-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-26
      • 1970-01-01
      • 2017-09-10
      相关资源
      最近更新 更多