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