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#),主要是因为您不能轻松地创建类型的联合或交集,并且因为在泛型中有 extends
和 super
约束起作用作为协方差和逆变的标记。至少在 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. U
或U ext. T
,则允许分配具有至少一个逆变相关成员(例如setValue(a: T)
)的泛型let x: ISetter<T> = <ISetter<U>> y
。一个没有成员的泛型依赖于T
,它的类型参数不受限制。
我还发现下面的编译不出来。 let numberBox: set: (x: number) => void = set: () => null ; function insertString(items: set: (x: string | number) => void ): void items.set('Test'); insertString(numberBox);
这个case好像和题中的例子不符。
...见my newly created TypeScript issue #20738。
您的let x: ISetter<T> = <ISetter<U>>y
示例使用了类型断言,这比推断intentionally 更宽松。实际上,类型断言只在不安全的向下转换方向上才需要。
“您的let x: ISetter<T> = <ISetter<U>>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 个语句中 IBox
被 Box
替换,则逆变类型检查不适用。因此,为了防止有人意外引用此类类型,Box<T>
和 IBox<T>
可以放在单独的 ES6 模块中,仅公开接口和工厂函数。
【讨论】:
以上是关于TypeScript 中泛型的不安全隐式转换的主要内容,如果未能解决你的问题,请参考以下文章