Typescript 限制泛型参数中的属性协方差
当我使用一些强制属性指定对象类型的泛型参数时(例如,这里我有一个函数要求对象具有Typescript 限制泛型参数中的属性协方差,typescript,generics,typescript-generics,Typescript,Generics,Typescript Generics,当我使用一些强制属性指定对象类型的泛型参数时(例如,这里我有一个函数要求对象具有timestamp:string来为其分配时间戳),typescript允许使用更具体的属性类型作为泛型参数-参见下面的示例 有没有办法限制这一点?我认为这违反了继承原则,因为对象属性是读/写的,所以它们应该保持类型优先于继承,并且不允许协变类型 function updateTimestamp<T extends {timestamp: string}>(x: T): T { return {.
timestamp:string
来为其分配时间戳),typescript允许使用更具体的属性类型作为泛型参数-参见下面的示例
有没有办法限制这一点?我认为这违反了继承原则,因为对象属性是读/写的,所以它们应该保持类型优先于继承,并且不允许协变类型
function updateTimestamp<T extends {timestamp: string}>(x: T): T {
return {...x, timestamp: new Date().toDateString()};
}
type Bar = {timestamp: 'not today'};
const b: Bar = updateTimestamp<Bar>({timestamp: 'not today'})
console.log(b); // {timestamp: (actual timestamp))}, should not match Bar
函数updateTimestamp(x:T):T{
返回{…x,时间戳:new Date().toDateString()};
}
输入Bar={timestamp:'nottoday'};
常量b:Bar=updateTimestamp({timestamp:'不是今天'})
控制台日志(b);//{timestamp:(实际时间戳))},不应与条匹配
类型脚本(有意地)是这样的:您可以将a
类型的值分配给B
类型的变量,其中a扩展B
,属性值被认为是协变的(如果a扩展B
,那么对于任何公共属性键K
,a[K]扩展B[K]
),并且您可以修改非只读属性。这允许不可靠的属性写入。我不知道是否有关于这方面的规范文档;但是GitHub的一些问题,比如说和谈论它。它肯定违反了继承原则,但根据TypeScript团队的说法,严格执行这些原则会使语言使用起来更加烦人。所以在某种程度上,这是不可避免的
但是,您可以更改updateTimestamp()
的定义以避免此问题。一种方法是注意,虽然updateTimeStamp()
将接受T
,但它返回的不一定是T
。相反,它是一个{[K in keyof T]:K扩展了“timestamp”?string:T[K]}
,或者等效地,省略&{timestamp:string}
:
function updateTimestamp<T extends { timestamp: string }>(
x: T
): Omit<T, "timestamp"> & { timestamp: string } {
return { ...x, timestamp: new Date().toDateString() };
}
如果传入的对象的时间戳
是宽字符串
类型,则该对象将工作:
let okay = { timestamp: "yesterday" };
/* let okay: {
timestamp: string;
} */
okay = updateTimestamp(okay); // okay
还有其他可能的方法来更改updateTimestamp()
,以表达您的意图。也许您实际上想要禁止接受T
的timestamp
属性小于字符串的T
。这很难表达,但有可能:
function updateTimestamp<T extends {
timestamp: string & (string extends T["timestamp"] ? unknown : never)
}>(
x: T
): T {
return { ...x, timestamp: new Date().toDateString() };
}
幸运的是,您正在返回一个新值,而不是修改现有值。这意味着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();
}
任何事情都无法阻止以下情况的发生:
const sneakyBar: { timestamp: string } = myBar;
updateTimestamp(sneakyBar); // not rejected!
myBar.timestamp // "not today" at compile time, but string at runtime
允许将条
值分配给{timestamp:string}
变量,并且允许写入属性。这只是语言的一部分,您对updateTimestamp()
函数所做的任何操作都不会改变这一点
正如我所说的,你的函数没有这个问题,因为它本身不会改变任何东西。但是请记住,TypeScript的类型安全性是有限制的,您非常接近其中之一
谢谢您详尽的回答。我担心我已经到了边缘,但我想确定没有直截了当的方法。不幸的是,我的实际代码要复杂得多,还碰到了其他类型脚本的流血边缘,如果没有“干净”的解决方案,合理地处理它将是一件非常痛苦的事情,这只是我问题的一部分,但我从你的回答中得到了一些启发:)
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