TypeScript {} 类型被认为与任何其他非空类型相同
我有一个处理器类,其中有一个 concat 函数,它接收另一个处理器实例,对其进行处理,然后再次返回另一个处理器实例。
我希望 concat 函数接受一个与 V 泛型类型相同的连接器参数。这在大多数情况下都能很好地工作,但如果我使用 {} 类型就不行了。这本不应该可行,但它却可行。我也尝试过使用 NoInfer,但它不起作用。
附注:我知道 {} 类型表示任何非空类型,所以我需要它抛出编译错误,因为它理论上可以接受任何非空类型,而我不想这样,因为我希望它是相同的 V 类型。
我该怎么办??
代码:
class Processor<T, V> {
current: T
next : V
constructor(current: T, next: V) {
this.current = current
this.next = next
}
concat<U>(concatenator: Processor<U, V>): Processor<U, V> {
// Do something
// Fake return
return '' as unknown as Processor<U, V>
}
}
let c: Processor<string, {}> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a)
答案#1
你的 Processor<T, V> 类型在 V 上是 协变 的(参见 TypeScript 中的方差、协变、逆变、双向变和不变的区别),所以如果 V1 extends V2,那么 Processor<T, V1> extends Processor<T, V2>。并且由于空类型 {} 确实表示任何非 nullish 值,并且由于 string extends {},那么 Processor<string, string> extends Processor<string, {}>。这一切都是 TypeScript 按设计工作的,如果它不需要在运行时产生错误,那么可以说你应该保持这样。
TypeScript 的类型系统没有 microsoft/TypeScript#12936 中请求的“精确类型”,所以你不能轻易地说“这些属性而且仅这些”,因此没有简单的 Exact<{}> 可以用来替代 {} 来达到这个目的。嗯,有一个接近的方法;你可以尝试使用 {[k: string]: never} 来代替 {}:
let c: Processor<string, { [k: string]: never }> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a); // error!
// Type 'string' is not assignable to type '{ [k: string]: never; }'.
但是 {[k: string]: never} 很脆弱,并且它不能扩展到具有有限非零数量属性的情况。
如果你的用例确实要求 Processor<T, V1> extends Processor<T, V2> 当且仅当 V1 和 V2 是相同类型(或可相互赋值)时成立,那么你想要的是 Processor<T, V> 在 V 上是不变的。有一些方式可以自然地实现这一点,如果类的某个成员在 V 上是逆变的,那么整个类就是不变的:
class Processor<T, V> {
next: V // next is covariant in V
randomThing?: (x: V) => void; // randomThing is contravariant in V
} // whole thing is invariant in V
let c: Processor<string, {}> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a); // error!
或者,如果你想手动确保更严格的方差,你可以在类型参数上使用方差注解。out 修饰符表示“非逆变”,in 修饰符表示“非协变”,因此不变性由 in out 指定(“既非逆变也非协变”):
class Processor<T, in out V> {
// ⋯
}
let c: Processor<string, {}> = new Processor('a', {})
let a: Processor<string, string> = new Processor('a', 'a')
let t = c.concat(a); // error!
我的建议是确保 Processor<T, V> 在 V 上自然是协变的,可以通过添加成员来保证这一点,而不是使用变体标记或更复杂的类型操作。如果你无法这样做,因为你真的不需要额外的成员,那么我会回到我最早的建议:如果没有因此出现实际的运行时错误,就保持现状。TypeScript 的类型系统并不完美,但协变是有用的,如果 TypeScript 认为 Processor<T, V> 在 V 上是协变的,你实际上可能会从中受益。 你真的希望 TypeScript 在它推断出 Processor<T, "a"> 而你想使用的是 Processor<T, string> 时抱怨吗?如果 V 是不变的,那么这两种类型是无关的。如果你认为 Processor<T, "a"> 扩展 Processor<T, string> 是好事情,那么就保持你的代码不变,并以其他方式处理 {}。
代码的 Playground 链接
