为什么类型保护类型的差价会导致类型检查被跳过?

Law*_*eld 7 typescript typescript-generics

题:

为什么在T使用对象扩展构造所述对象时忘记将嵌套字段添加到类型的对象时不会收到编译时错误?

例子:

interface User {
  userId: number;
  profile: {
    username: string
  }
}

function updateUsername(user: User): User {
  return {
    ...user,
    profile: {
      // Error (as expected)
    }
  }
}

function updateUsernameGeneric<T extends User>(user: T): T {
  return {
    ...user,
    profile: {
      // No error (unexpected... why?)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

我自己对答案的猜测:

我可以想像的是,打字稿允许亚型删除其超级计算机的性能,从而有可能对某些亚型TUserprofile财产可能不包含任何属性。(如果是这样,我不知道 TypeScript 允许你这样做......)


打字稿版本 4.1.2

操场

Ole*_*ter 2

这与普通类型(参见PR )与普通类型相比如何解决扩展有关。如果将结果对象写入变量,您将立即注意到差异:对于非泛型类型,合并类型被推断为:

{
    profile: {};
    userId: number;
}
Run Code Online (Sandbox Code Playgroud)

这导致该类型无法分配给User具有必需username子属性的带注释的返回类型。这正是编译器错误 TS 2322 告诉您的内容:

类型“{}”中缺少属性“username”,但类型“{ username: string;”中需要属性“username” }'

现在,泛型的情况有点不同:类型实际上被推断为 的子类型User{ profile: {}; }类型的交集:

T & {
    userId: string;
    profile: {};
}
Run Code Online (Sandbox Code Playgroud)

编译器对此表示同意,因为交集是带注释的返回类型的“扩展”,其中包含交集定义的所有属性。


这是否是一个好的行为是有争议的,因为您可以执行以下操作,并且编译器不会更明智:

function updateUsernameGeneric<T extends User>(user: T): T {
  const newUser = {
    ...user,
    userId: "234",
    profile: {
      // No error (unexpected... why?)
    }
  }

  return newUser;
}

updateUsernameGeneric({ profile: { username: "John" }, userId: 123 }).userId //a-ok, "number"
Run Code Online (Sandbox Code Playgroud)

由于返回类型是泛型类型参数和合并属性的交集,因此您可以取消对返回类型的注释并让 TypeScript 推断它。不兼容的属性类型将被正确推断为never

function updateUsernameGenericFixed<T extends User>(user: T) {
  const newUser = {
    ...user,
    userId: "234",
    profile: {
      // No error (unexpected... why?)
    }
  }

  return newUser;
}

updateUsernameGenericFixed({ profile: { username: "John" }, userId: 123 }).userId //never
Run Code Online (Sandbox Code Playgroud)

操场