将枚举值用作通用参数不适用于值大于一个的枚举

时间:2018-11-28 19:09:57

标签: typescript

我发现一种情况,如果打字稿中的枚举具有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
})

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

所以我的假设是,当枚举只有一个值时,打字稿会推断类型TActionTypes.A。但是,如果枚举具有一个以上的值,那么Typescript无法再推断出这个值吗? 但是为什么会这样呢?在给定的示例中,TActionTypes.A的信息在对象文字

中明确定义
{
    type: ActionTypes.A
}

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

1 个答案:

答案 0 :(得分:3)

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

在您的情况下,您想将T的通用类型参数enhance()推导为狭义文字类型ActionTypes.A,但实际上将其推导为{{ 1}},即构成ActionTypes的每种文字类型的并集。如果enum是唯一元素,则类型ActionTypes.AActionTypes是相同的,那么您就不会遇到问题。但是,当ActionTypes.A等效于ActionTypes时,就会遇到问题,因为ActionTypes.A | ActionTypes.B的返回类型变为enhance(),而不是您期望的SpecialAction<ActionTypes>。而且,您必须先检查SpecialAction<ActionTypes.A>或使用类型断言才能将SpecialAction<ActionTypes>的值分配给SpecialAction<ActionTypes.A>

同样,您希望将T推断为ActionTypes.A。确实,如果像T中那样手动指定类型参数enhance<ActionTypes.A>(...),它就可以工作。我们如何让编译器推断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

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