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
这对你有用吗?
- 创建类型为联合类型(字符串字面量类型的联合)而不是枚举类型:
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>>;
- 创建可以在运行时使用的值:
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");
- 然后我们可以使用这些类型和值:
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
查看此演示
