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

时间:2017-12-16 22:28:27

标签: typescript generics types type-conversion tsc

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以便识别出这样的错误吗?

2 个答案:

答案 0 :(得分:3)

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

这个问题在Java中并没有显示出来(不确定C#)主要是因为你不能轻易地创建类型的联合或交集,并且因为在泛型中有extends和{{1}作为协方差和逆变的标记的约束。你确实在Java数组中看到了这一点,至少它们被认为是不合理的协变(尝试上面的superObject[],你会看到有趣的事情发生。)

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

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

Integer[]

除了“小心”之外,我没有很好的建议。 TypeScript是not intended,具有严格的声音类型系统(参见非目标#3);相反,语言维护者倾向于仅在程序中引起实际错误的程度上解决问题。如果这种事情对你影响很大,可能会转到Microsoft/TypeScript#10717或类似的问题,如果你觉得它具有说服力,可以给它一个或描述你的用例。

希望这有帮助。祝你好运!

答案 1 :(得分:0)

由于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<T>IBox<T>可以放在单独的ES6模块中,只显示接口和工厂函数。