TypeScript 中泛型的不安全隐式转换

Posted

技术标签:

【中文标题】TypeScript 中泛型的不安全隐式转换【英文标题】:Unsafe implicit conversion of generics in TypeScript 【发布时间】: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 以便识别这样的错误吗?

【问题讨论】:

【参考方案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好像和题中的例子不符。 ...见my newly created TypeScript issue #20738。 您的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 模块中,仅公开接口和工厂函数。

【讨论】:

以上是关于TypeScript 中泛型的不安全隐式转换的主要内容,如果未能解决你的问题,请参考以下文章

带有泛型的 Typescript JSX - 参数隐式具有“任何”类型

JAVA泛型

java中泛型的使用

学习笔记——泛型

Java中泛型的使用

java泛型与object的比较