网站搜索

TypeScript 泛型类型的部分类型


我想将一个类型定义为显式属性与泛型类型的混合,如果键重复,则显式属性优先。下面是我的尝试,但我在标注的那一行遇到了错误——有人能解释原因吗,还是这是 TypeScript 编译器的一个 bug?

// Takes properties in A and B. For matching properties, the types in A are used.
type Mix<A, B> = {
  [K in keyof B | keyof A]: K extends keyof A
    ? A[K]
    : K extends keyof B
    ? B[K]
    : never
}

type Versionable = { version: number }

function test<T>(): void {
  const version1: Partial<Mix<Versionable, T>>['version'] = 1 // compiles - version type is correctly inferred as number | undefined
  const version2: Partial<Mix<Versionable, T>>['version'] = undefined // compiles
  const version3: Partial<Mix<Versionable, T>>['version'] = '1' // does not compile as expected

  const obj1: Partial<Mix<Versionable, T>> = { version: 1 } // DOES NOT COMPILE.... WHY??
  const obj2: Partial<Mix<Versionable, T>> = { version: undefined } // compiles
  const obj3: Partial<Mix<Versionable, T>> = { version: '1' } // does not compile as expected
  const obj4: Partial<Mix<Versionable, T>> = {} // compiles
  obj4.version = 1 // compiles
}

答案#1

我认为这里的行为与 microsoft/TypeScript#13442 中的情况相同;几乎没有具体值被编译器视为可分配给 Partial<SomethingDependingOnAGenericTypeParam>,除了那些属性类型为 undefined 的值(这是 obj2 行为的原因)或空对象(这是 obj4 行为的原因)。

这个限制通常是可以辩护的,因为这些任务往往是不安全的:

function unsafeCaught<T extends { a: string }>() {
  const nope: Partial<T> = { a: "" }; // CORRECT error  
}
interface Oops { a: "someStringLiteralType" };
unsafeCaught<Oops>(); // {a: ""} is not a valid Partial<Oops>

(这是负责 obj3 行为的原因。)但是该限制也阻止了一些已知安全的对象,例如你的对象,它是经过特别设计的:

function safeCaught<T extends { a: string }>() {
  const stillNope: { [K in keyof T]?: K extends "a" ? string : T[K] }
    = { a: "" }; // incorrect error
}

(这是负责 obj1 行为的原因。) 在这些情况下,编译器根本没有执行使其工作的必要分析。一个类似的问题,microsoft/TypeScript#31070,实际上被认为是一个错误并已修复,但在那个案例中,映射属性类型是一个像 number 的常量,而在你的情况下是一个条件类型。而且编译器在验证依赖未解析泛型参数的条件类型的可赋值性方面已经做得相当糟糕。因此,对于你的情况,我会将其归结为 TypeScript 的设计限制,并使用类型断言来表明你在这种情况下比编译器更清楚:

const obj1 = { version: 1 } as Partial<Mix<Versionable, T>>; // okay now

奇怪的是,当你向一个属性写入而不是将对象赋值给变量时,这个限制会被放宽,因此你最终会得到完全相同的错误行为而不会报错:

function unsafeUncaught<T extends { a: string }>(val: Partial<T>) {
  val.a = ""; // uh, wait
}
const oops: Oops = { a: "someStringLiteralType" };
unsafeUncaught(oops);

我不确定为什么,但这可能是某个地方的设计决定。因此,下面的代码也不会报错,但只是巧合,因为它本身并没有正确地进行检查:

function safeUncaught<T extends { a: string }>(
  val: { [K in keyof T]?: K extends "a" ? string : T[K] }
) {
  val.a = ""; // okay but coincidentally
}

这大概就是为什么你的 version1、version2 和 version3 可行,以及 obj4.version=1 也可行的原因。


代码链接