【问题标题】:Restrict property covariance in generic argument限制泛型参数中的属性协方差
【发布时间】:2020-09-02 23:44:56
【问题描述】:

当我指定具有一些强制属性的对象类型的通用参数时(例如,这里我有一个函数要求对象具有timestamp: string 为其分配时间戳),打字稿允许使用更具体的属性类型作为通用参数 - 请参见下面的示例。

有没有办法限制这个?我认为它违反了继承原则,因为对象属性是读/写的,它们应该保持它们的类型而不是继承,并且不允许协变类型。

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

playground link

【问题讨论】:

    标签: typescript generics typescript-generics


    【解决方案1】:

    TypeScript 是(故意)unsound 这种方式:您可以将A 类型的值分配给B 类型的变量,其中A extends B,属性值被认为是协变的(如果@987654328 @then 对于任何公共属性键KA[K] extends B[K]),并且您可以修改非readonly 属性。这允许不健全的属性写入。我不知道是否有关于此的规范文档;但是像 microsoft/TypeScript#8474microsoft/TypeScript#18770 这样的 GitHub 问题谈论它。它肯定违反了继承原则,但根据 TypeScript 团队的说法,严格执行这些原则会使语言使用起来更加烦人。所以在某种程度上这是不可避免的。


    但是,您可以更改updateTimestamp() 的定义来回避这个问题。一种方法是注意虽然updateTimeStamp() 将接受T,但它返回的不一定是T。相反,它是{[K in keyof T]: K extends "timestamp" ? string: T[K]},或等效的Omit&lt;T, "timestamp"&gt; &amp; { 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

    【讨论】:

    • 感谢非常详尽的回答。我担心我在这里碰到了边缘,但我想确定没有直接的方法。不幸的是,我的实际代码要复杂得多,还会遇到其他打字稿出血边缘,如果没有“干净”的解决方案,合理地处理它会很痛苦,这只是我的问题的一个 MWE,但我从你那里得到了一些灵​​感答案:)
    猜你喜欢
    • 1970-01-01
    • 2012-09-29
    • 2020-11-20
    • 2011-02-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多