【问题标题】:Why do spreads on type-guarded types cause type checks to be skipped?为什么类型保护类型的扩展会导致类型检查被跳过?
【发布时间】:2021-06-06 20:34:26
【问题描述】:

问题:

当我在使用对象扩展构造所述对象时忘记将嵌套字段添加到类型为 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?)
    }
  }
}

我自己对答案的猜测:

我所能想象的是,TypeScript 允许子类型删除它们的超级属性,这使得对于User 的某些子类型Tprofile 属性可能不包含任何特性。 (如果是这样,我不知道 TypeScript 允许你这样做......)


TypeScript 版本 4.1.2

Playground

【问题讨论】:

  • 这很有趣。我们实际上可以将您的泛型函数用于violate the type system,并在类型为非undefined 的地方获取undefined。这可能是编译器错误。
  • 是的,很有趣,即使{ ...user, profile: undefined } 也被接受了

标签: typescript typescript-generics


【解决方案1】:

这与使用泛型类型(参见PR)与普通类型相比如何解决传播有关。如果将结果对象写入变量,您会立即注意到不同之处:对于非泛型类型,合并类型推断为:

{
    profile: {};
    userId: number;
}

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

类型“{}”中缺少属性“用户名”,但类型“{用户名:字符串; }'

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

T & {
    userId: string;
    profile: {};
}

编译器可以接受,因为交集是带注释的返回类型的“扩展”,包含根据交集定义的所有属性。


这是否是一个好的行为值得商榷,因为您可以执行以下操作,编译器也不会更明智:

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"

由于返回类型是泛型类型参数和合并属性的交集,您可以取消注释返回类型并让 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

Playground

【讨论】:

  • 即使这无助于打字稿检测return { ...user, profile: {username: undefined} };
  • @smac89 - 从技术上讲,如果您认为profile 属性本身上的never 是可接受的解决方法(这次整个profile 将是never 类型,请参阅playground)。坦率地说,我同意这看起来确实是不好的行为。也就是说,我还没有找到可以谈论这个问题的问题/FR,介绍该功能的 PR 也没有关于该行为的任何报告。
  • 我应该在 GitHub 上提出这个问题吗?我喜欢您的解决方法,但是由于 ESLint 规则,我不能将返回类型从方法中移除,此外,我需要记住逐个案例执行此操作的事实在某种程度上违背了目标:我会需要有意识地意识到我“不小心”违反了类型保护以删除返回类型,此时我只需修复代码(即包括缺少的属性,并保留返回类型):) 谢谢为了您的深入解释,奥列格!
  • @Lawrence - NP!是的,开始一个问题可能是个好主意。我没有在 repo 上找到一个提高的,所以除非有人附上链接,否则这似乎是新事物。这种行为当然很奇怪,而且解决方法也不是很方便,就像你提到的那样
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多