TypeScript 是(故意)unsound 这种方式:您可以将A 类型的值分配给B 类型的变量,其中A extends B,属性值被认为是协变的(如果@987654328 @then 对于任何公共属性键K,A[K] extends B[K]),并且您可以修改非readonly 属性。这允许不健全的属性写入。我不知道是否有关于此的规范文档;但是像 microsoft/TypeScript#8474 和 microsoft/TypeScript#18770 这样的 GitHub 问题谈论它。它肯定违反了继承原则,但根据 TypeScript 团队的说法,严格执行这些原则会使语言使用起来更加烦人。所以在某种程度上这是不可避免的。
但是,您可以更改updateTimestamp() 的定义来回避这个问题。一种方法是注意虽然updateTimeStamp() 将接受T,但它返回的不一定是T。相反,它是{[K in keyof T]: K extends "timestamp" ? string: T[K]},或等效的Omit<T, "timestamp"> & { timestamp: string }:
function updateTimestamp<T extends { timestamp: string }>(
x: T
): Omit<T, "timestamp"> & { timestamp: string } {
return { ...x, timestamp: new Date().toDateString() };
}
编译器实际上可以验证实现是否符合Omit 版本,所以我使用了它。现在,如果你调用Bar 代码,你会得到你所期望的错误:
const myBar: Bar = { timestamp: 'not today' };
const newBar = updateTimestamp(myBar);
/* const newBar: Pick<Bar, never> & {
timestamp: string;
} */
const b: Bar = newBar; // error!
// -> ~
// Type 'string' is not assignable to type '"not today"'.
如果你传入一个 timestamp 是宽 string 类型的对象,它将起作用:
let okay = { timestamp: "yesterday" };
/* let okay: {
timestamp: string;
} */
okay = updateTimestamp(okay); // okay
还有其他可能的方法可以更改updateTimestamp() 以表达您的意图。也许您实际上想禁止接受timestamp 属性比string 窄的T。这更难表达但可能:
function updateTimestamp<T extends {
timestamp: string & (string extends T["timestamp"] ? unknown : never)
}>(
x: T
): T {
return { ...x, timestamp: new Date().toDateString() };
}
然后你会得到这种行为:
const newBar = updateTimestamp(myBar); // error!
// --------------------------> ~~~~~
// Type '"not today"' is not assignable to type 'never'.(2345)
okay = updateTimestamp(okay); // okay
幸运的是,您正在返回一个新值,而不是修改现有值。这意味着updateTimestamp() 的输出基本上独立于其输入,因此您不必担心子类型不健全会传播到输出中。例如,假设updateTimestamp() 实际上设置其输入的timestamp 值:
function updateTimestamp<T extends {
timestamp: string & (string extends T["timestamp"] ? unknown : never)
}>(x: T) {
(x as { timestamp: string }).timestamp = new Date().toDateString();
}
那么尽管仍然会阻止以下情况:
// updateTimestamp(myBar); // this would be rejected
没有什么可以阻止以下事情的发生:
const sneakyBar: { timestamp: string } = myBar;
updateTimestamp(sneakyBar); // not rejected!
myBar.timestamp // "not today" at compile time, but string at runtime
您可以将Bar 值分配给{timestamp: string} 变量,并且允许写入属性。这只是语言的一部分,您对 updateTimestamp() 函数所做的任何事情都不会改变这一点。
正如我所说,你的函数没有这个问题,因为它本身不会改变任何东西。但请记住,TypeScript 的类型安全性是有限制的,您离其中之一很近。
Playground link to code