网站搜索

typescript - 不同枚举的通用函数


我有多个枚举

const enum E1 {
    CLOSED = 'CLOSED',
    OPEN = 'OPEN',
    IN_PROGRESS = 'IN_PROGRESS',
}

const enum E2 {
    OPEN = 'OPEN',
    CLOSED = 'CLOSED',
    IN_PROGRESS = 'IN_PROGRESS',
}

const enum E3 {
    OPEN = 'OPEN',
    IN_PROGRESS = 'IN_PROGRESS',
}

我想创建一个函数,该函数接受一个枚举,其中一个值应该是 'CLOSED'

const func = (value: 'CLOSED') => {...}
interface Data1 {status: E1}
interface Data2 {status: E2}
interface Data3 {status: E3}
const o1: Data1 = {status: E1.CLOSED}
const o2: Data2 = {status: E2.OPEN}
const o3: Data3 = {status: E3.OPEN}

func(o1.status)   // this should be valid. status is type E1, it contains 'CLOSED'
func(o2.status)   // this should be valid. status is type E2, it contains 'CLOSED'
func(o3.status)   // this should be invalid. status type is E3, it does not contain 'CLOSED'

不,它会为每一个抛出错误:

Argument of type E... is not assignable to parameter of type "CLOSED"

我不希望那个函数知道每个枚举,因为它们太多了。这就是为什么我使用了类型 value: 'CLOSED'

我是否需要类似一个公共枚举的东西,以便其他人以某种方式扩展它,这样我就可以在参数中使用它的类型,而不是使用 'CLOSED'?

TypeScript 版本:4.9.4

答案#1

枚举有一些奇怪的类型系统行为,这使得事情更复杂。现在,先使用简单的字符串字面量类型来代替枚举,然后我们可以再回过头来处理它。我们暂时可以用 const 断言的对象和类型别名来替换你的枚举定义。E1 看起来像

const E1 = {
    CLOSED: 'CLOSED',
    OPEN: 'OPEN',
    IN_PROGRESS: 'IN_PROGRESS',
} as const
type E1 = typeof E1[keyof typeof E1]

而其他的是类似的。


类型 (value: "CLOSED") => void 对你不起作用,因为这意味着你传入的 value 必须可赋值给字面量类型 "CLOSED"。这与你想要的相反。相反,你想表达的是 "CLOSED" 必须可以被赋值给 value 的类型。不是从上界限制 value 的类型(例如,它必须是 "CLOSED" 或某种更窄的类型),而是从下界限制它(例如,它必须是 "CLOSED" 或某种更宽的类型)。

不幸的是,TypeScript 本身并不原生支持这种下界类型约束。TypeScript 的约束(例如 T extends U)是上界。如果希望增加下界约束(例如 T super U),微软的 TypeScript 项目中有一个开放的功能请求 microsoft/TypeScript#14520。如果 TypeScript 支持了这个功能,那么我会说你可以写

// not valid TS, don't try this:
const func = <T extends string super 'CLOSED'>(value: T) => { }

然后完成。你将通过 string 从上方约束 value,通过 "CLOSED" 从下方约束 value。但是你不能直接这样做。

作为一种变通办法,你可以在约束中利用条件类型。从概念上讲,T super "CLOSED" 与 "CLOSED" extends T 是相同的。因此,如果我们可以重写约束以强制执行 "CLOSED" extends T,那么它可能会按你期望的方式表现:

const func = <T extends 'CLOSED' extends T ? string : never>(value: T) => { }

func(o1.status)   // okay
func(o2.status)   // okay
func(o3.status)   // error
func(Math.random() < 0.5 ? "CLOSED" : "XYZ"); // okay
func(Math.random() < 0.5 ? "ABC" : "XYZ"); // error

它确实可行。当你调用 func(o1.status) 时,T 被推断为 E1。然后约束变为 'CLOSED' extends E1 ? string : never,它会简化为 string,因此约束为 T extends string,条件满足,因此调用成功。

但是当你调用 func(o3.status) 时,T 会被推断为 E3。然后约束变为 'CLOSED' extends E3 ? string : never,它会简化为 never 类型,所以约束就是 T extends never,这是 未满足 的,因此调用失败。


如果你不使用枚举,我就是这样做的。枚举让事情更复杂,因为它们本身大多被认为是其值的适当子类型。所以E1.CLOSED extends "CLOSED"为真,但"CLOSED" extends E1.CLOSED为假:

type Yes = E1.CLOSED extends "CLOSED" ? true : false
//   ^? type Yes = true
type No = "CLOSED" extends E1.CLOSED ? true : false
//   ^? type No = false

这意味着"CLOSED"并不是E1的下界,尽管它是E1所表示的字符串字面量类型的下界。与其尝试解析枚举类型,我会使用一个技巧将字符串枚举拓宽到它们的字符串字面量类型:只需使用模板字面量类型将它们序列化即可:

type E1Serialized = `${E1}`
//   ^? type E1Serialized = "CLOSED" | "OPEN" | "IN_PROGRESS"

因此 "CLOSED" 是 E1Serialized 的下界。这意味着我们可以将 func 改为

const func = <T extends 'CLOSED' extends `${T}` ? string : never>(value: T) => { }

而且一切都能工作:

func(o1.status)   // okay
func(o2.status)   // okay
func(o3.status)   // error
func(Math.random() < 0.5 ? "CLOSED" : "XYZ"); // okay
func(Math.random() < 0.5 ? "ABC" : "XYZ"); // error

代码的 Playground 链接

答案#2

这对你有用吗?

  1. 创建类型为联合类型(字符串字面量类型的联合)而不是枚举类型:
type CLOSED<T extends string> = `${T}.CLOSED`;
type OPEN<T extends string> = `${T}.OPEN`;
type IN_PROGRESS<T extends string> = `${T}.IN_PROGRESS`;

type EWithClosed<T extends string> = CLOSED<T> | OPEN<T> | IN_PROGRESS<T>;
type EWithoutClosed<T extends string> = Exclude<EWithClosed<T>, CLOSED<T>>;
  1. 创建可以在运行时使用的值:
const makeClosed = <T extends string>(k: T): CLOSED<T> => `${k}.CLOSED`;
const makeOpen = <T extends string>(k: T): OPEN<T> => `${k}.OPEN`;
const makeInProgress = <
    T extends string
>(k: T): IN_PROGRESS<T> => `${k}.IN_PROGRESS`;

const makeWithClosed = <T extends string>(k: T) => ({
  CLOSED: makeClosed(k),
  OPEN: makeOpen(k),
  IN_PROGRESS: makeInProgress(k)
});

const makeWithoutClosed = <T extends string>(k: T) => ({
  CLOSED: makeClosed(k),
  OPEN: makeOpen(k)
});

const E1 = makeWithClosed("E1");
const E2 = makeWithClosed("E2");
const E3 = makeWithoutClosed("E3");
  1. 然后我们可以使用这些类型和值:
const o1 = {status: E1.CLOSED }
const o2 = {status: E2.OPEN }
const o3 = {status: E3.OPEN }

const func = <T extends string>(value: CLOSED<T>) => {
  // ...
}

func(o1.status)   // OK
func(o2.status)   // ERROR
func(o3.status)   // ERROR

查看此演示