【问题标题】:Use enum value as generic parameter doesn't work with enums with more then one value使用枚举值作为通用参数不适用于具有多个值的枚举
【发布时间】:2018-11-29 03:53:01
【问题描述】:

我发现了一种情况,如果 typescript 中的 Enum 有 1 个或多个值,但我不明白为什么会有所不同。

看看这个简化的示例代码:

// library code

interface Action<T = any> {
    type: T
}

interface SpecialAction<T = any> extends Action<T> {
    id: string
    timestamp: Date
}

function enhance<T>(action: Action<T>): SpecialAction<T> {
    return {
        ... action,
        id: "some-id",
        timestamp: new Date()
    }
}

// users code

enum ActionTypes {
    A = "A"
}

interface UserAction extends SpecialAction<ActionTypes.A> {}

const value: UserAction = enhance({
    type: ActionTypes.A
})

这工作得很好。但是,如果我像这样更改枚举:

enum ActionTypes {
    A = "A",
    B = "B"
}

然后我在const value: UserAction = enhance({ 的行中收到以下编译错误:

Type 'SpecialAction<ActionTypes>' is not assignable to type 'UserAction'.
    Types of property 'type' are incompatible.
        Type 'ActionTypes' is not assignable to type 'ActionTypes.A'.

如果我将代码更改为:

const value: UserAction = enhance<ActionTypes.A>({
    type: ActionTypes.A
})

错误消失了,一切都恢复正常了。

所以我的假设是,当枚举只有一个值时,打字稿将类型 T 推断为 ActionTypes.A。但是如果枚举的值不止一个,那么 Typescript 就不能再推断了吗? 但为什么会这样呢?在给定的示例中,TActionTypes.A 的信息在对象文字中明确定义

{
    type: ActionTypes.A
}

但更普遍的问题是: 为什么枚举值的数量对编译器很重要? 这不是很危险吗,因为它可能会以意想不到的方式破坏行为?

【问题讨论】:

    标签: typescript


    【解决方案1】:

    当 TypeScript 遇到文字 string/number/enum 类型的值时,会发生很多事情。有时编译器会将类型扩大到string/number/the-relevant-enum。其他时候,编译器会将类型保留为文字值。描述哪一个会发生在哪里并不容易。如果你敢,你可以read all about it

    在您的情况下,您在enhance() 中有一个泛型类型参数T,您希望将其推断为狭义文字类型ActionTypes.A,但实际上推断为ActionTypes,每个的并集构成enum 的文字类型。如果ActionTypes.A 是唯一的元素,那么ActionTypesActionTypes.A 类型是相同的,您不会发现问题。但是当ActionTypes 等价于ActionTypes.A | ActionTypes.B 时,你就会遇到问题,因为enhance() 的返回类型变成了SpecialAction&lt;ActionTypes&gt;,而不是你期望的SpecialAction&lt;ActionTypes.A&gt;。如果不先检查或使用类型断言,您不能将 SpecialAction&lt;ActionTypes&gt; 值分配给 SpecialAction&lt;ActionTypes.A&gt;

    同样,您希望T 被推断为ActionTypes.A。实际上,如果您手动指定类型参数T,如enhance&lt;ActionTypes.A&gt;(...),它就可以工作。我们如何让编译器推断T 的窄类型?好吧,事实证明,如果您在泛型类型上放置一个包含stringnumber 的特殊constraint,编译器会将其视为您的意思是该类型尽可能窄的提示。例如:

    declare function wide<T>(t: T): T;
    let s = wide("abc"); // s is of type string
    let n = wide(123); // n is of type number
    declare function narrow<T extends string>(t: T): T;
    let ss = narrow("abc"); // ss is of type "abc"
    let nn = narrow(123); // error, 123 does not extend string ?
    

    特殊约束甚至可以是类型的联合,只要联合的元素之一包含stringnumber 并且不被其他东西吸收:

    type Narrowable = string | number | boolean | symbol | object | void | undefined | null | {};
    declare function betterNarrow<T extends Narrowable>(t: T): T; 
    let sss = betterNarrow("abc"); // sss is of type "abc"
    let nnn = betterNarrow(123); // nnn is of type 123
    let bbb = betterNarrow(true); // bbb is of type true
    

    这里,Narrowableunknownany 基本相同,因为它几乎可以分配任何东西。但由于它是包含stringnumber 作为元素的事物的联合,它可以作为缩小通用约束的提示。 (请注意,type Narrowable = string | number | unknown 不起作用,因为它只会变成 unknown。这很棘手。)

    所以最后我们可以改变enhance() 来给它缩小提示:

    type Narrowable = string | number | boolean | symbol | object | void | undefined | null | {};
    function enhance<T extends Narrowable>(action: Action<T>): SpecialAction<T> {
      return {
        ...action,
        id: "some-id",
        timestamp: new Date()
      }
    } 
    
    const value: UserAction = enhance({
      type: ActionTypes.A
    }) // no error
    

    有效!希望有帮助。祝你好运。

    【讨论】:

    • 非常感谢您的详细解释。我还没有完全理解窄化的东西,但它有很大帮助,我会玩弄它来更好地理解细节。
    猜你喜欢
    • 2012-11-05
    • 2023-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多