Typescript 强制从父对象(如枚举)使用对象字符串
Typescript enforce object string used from parent object like enum
我正在为一个较低级别的库开发一个 API 包装器,它使用枚举将人类可读的键映射到底层值。在我们的 API 中,我想屏蔽 logging/etc 中的所有基础值,所以我只想在我们这边使用枚举键。我想动态创建一个对象,它采用原始枚举并生成一个 keys/values 相等的新对象。我还想强制将父对象键像枚举一样传递到参数中,而不是允许直接使用字符串值。
enum Colors {
red = '$r5',
green = '$g2',
lightBlue = '$b9',
darkBlue = '$b1',
}
function getColor(color: Colors) {
return color;
}
getColor(Colors.darkBlue); // passes
getColor('darkBlue'); // fails as expected
function createKeyEnum<E>(e: E): {[k in keyof E]: k extends keyof E ? k : never} {
return Object.keys(e).reduce((p, v) => {
(p as any)[v] = v;
return p;
}, {} as {[k in keyof E]: k extends keyof E ? k : never});
}
const NewColors = createKeyEnum(Colors);
type NewColors = keyof typeof NewColors;
// forward mapping
function getOldColor(color: NewColors) {
return Colors[color];
}
// reverse mapping
function getNewColor(color: Colors) {
const reverse: any = {};
Object.entries(Colors).forEach(([k, v]) => reverse[v] = k);
return reverse[color] as NewColors;
}
getOldColor(NewColors.darkBlue); // passes
getOldColor('darkBlue'); // passes, but should fail like an enum
const color = getNewColor(Colors.darkBlue);
// typeof color == 'red' | 'green' | 'lightBlue' | 'darkBlue'
// should be: typeof color == NewColors
这对任何花哨的打字都是可能的,还是枚举是一种无法使用其他 TS 打字复制的纯粹的幕后魔法?在下面的示例中,我的 NewColors 类型(可以理解)只是文字键字符串的并集。虽然这有效 - 它确实允许用户直接使用字符串值。我怎样才能让它链接到父符号 NewColors 并强制值像枚举一样直接从它派生?
您希望 createKeyEnum()
输出的对象具有值为 nominally typed 的属性。因此,虽然 NewColors.darkBlue
在运行时可能等同于 "darkBlue"
,但您希望编译器将它们视为不同的,因为它们具有不同的 names 或 声明站点.
相比之下,大多数 TypeScript 类型系统 structural, where two types can be seen as compatible despite having different names or declaration sites. There is a longstanding open feature request at microsoft/TypeScript#202 要求对名义类型提供一些官方支持。
如您所见,enums 是一项功能,其中有一定数量的名义打字。但是您不能以编程方式生成枚举,因此这不会直接为您工作。
您可以做的是"brand" a string literal type by intersecting它带有一些名义上的或名义上的类型。这种品牌模拟类型系统中的标称类型。
使用交集意味着品牌类型将被视为可分配给字符串文字类型,但反之则不然。 (所以 NewColors extends 'red' | 'green' | 'lightBlue' | 'darkBlue'
应该是真的,但是 'red' | 'green' | 'lightBlue' | 'darkBlue' extends NewColors
应该是假的)。
根据相交类型,您可能需要在 createKeyEnum()
的实现中使用 type assertion 来让编译器相信您确实拥有该类型的值。毕竟,实际值只是一个字符串,当我们使用字符串代替标称类型的值时,编译器会报错。因此可能需要一些错误抑制。
假设我们有一个名为 Brand<T, U>
的类型别名,它将 T
标记为某种类型,该类型也依赖于 U
(因此 Brand<"darkBlue", Colors>
和 Brand<"darkBlue", SomeOtherEnum>
将彼此不兼容)。我们可以担心下面如何定义Brand
。现在,让我们说我们拥有它。
然后你可以这样实现createKeyEnum()
:
function createKeyEnum<E>(e: E) {
class foo { private bar = 0 }
return Object.keys(e).reduce((p, v) => {
(p as any)[v] = v;
return p;
}, {} as { [K in keyof E]: Brand<K, E> }); // <-- need to assert here
}
并且因为您有意使 NewColors
与 keyof typeof Colors
在某种程度上不兼容,您可能需要在索引 Colors
时明确地将前者扩展为后者以避免错误:
function getOldColor(color: NewColors) {
const k: keyof typeof Colors = color; // widen here
return Colors[k]; // now we can use k as a key
}
这会给你想要的行为:
getOldColor(NewColors.darkBlue); // passes
getOldColor('darkBlue'); // fails
const color = getNewColor(Colors.darkBlue);
// const color: NewColors
那么,我们应该如何实现Brand
呢?最简单的方法是使用“幻影”属性:
type Brand<T, U> = T & { _tag: U };
这使用结构类型来模拟名义类型。没有什么能阻止某人实际添加 _tag
属性,但很难想象有人通过 getOldColor(Object.assign("darkBlue", { _tag: Colors }))
规避安全性。如果有人遇到这样的麻烦,也许你应该让他们去做。
如果你真的想阻止这种情况,你可以使用 class
和 a private
member:
declare class BrandClass<U> { private constructor(); private _tag: U };
type Brand<T, U> = T & BrandClass<U>
由于 _tag
成员是 private
,编译器将 BrandClass
类型视为标称类型,因此不使用名称就无法获得 BrandClass
实例BrandClass
:
getOldColor(Object.assign("darkBlue", { _tag: Colors })); // error
而且你实际上不能构造它,因为构造函数也是 private
:
getOldColor(Object.assign("darkBlue", new BrandClass<typeof Colors>())); // error
这使得某人几乎不可能在不使用类型断言的情况下生成编译器视为 Brand<T, U>
的值。
我正在为一个较低级别的库开发一个 API 包装器,它使用枚举将人类可读的键映射到底层值。在我们的 API 中,我想屏蔽 logging/etc 中的所有基础值,所以我只想在我们这边使用枚举键。我想动态创建一个对象,它采用原始枚举并生成一个 keys/values 相等的新对象。我还想强制将父对象键像枚举一样传递到参数中,而不是允许直接使用字符串值。
enum Colors {
red = '$r5',
green = '$g2',
lightBlue = '$b9',
darkBlue = '$b1',
}
function getColor(color: Colors) {
return color;
}
getColor(Colors.darkBlue); // passes
getColor('darkBlue'); // fails as expected
function createKeyEnum<E>(e: E): {[k in keyof E]: k extends keyof E ? k : never} {
return Object.keys(e).reduce((p, v) => {
(p as any)[v] = v;
return p;
}, {} as {[k in keyof E]: k extends keyof E ? k : never});
}
const NewColors = createKeyEnum(Colors);
type NewColors = keyof typeof NewColors;
// forward mapping
function getOldColor(color: NewColors) {
return Colors[color];
}
// reverse mapping
function getNewColor(color: Colors) {
const reverse: any = {};
Object.entries(Colors).forEach(([k, v]) => reverse[v] = k);
return reverse[color] as NewColors;
}
getOldColor(NewColors.darkBlue); // passes
getOldColor('darkBlue'); // passes, but should fail like an enum
const color = getNewColor(Colors.darkBlue);
// typeof color == 'red' | 'green' | 'lightBlue' | 'darkBlue'
// should be: typeof color == NewColors
这对任何花哨的打字都是可能的,还是枚举是一种无法使用其他 TS 打字复制的纯粹的幕后魔法?在下面的示例中,我的 NewColors 类型(可以理解)只是文字键字符串的并集。虽然这有效 - 它确实允许用户直接使用字符串值。我怎样才能让它链接到父符号 NewColors 并强制值像枚举一样直接从它派生?
您希望 createKeyEnum()
输出的对象具有值为 nominally typed 的属性。因此,虽然 NewColors.darkBlue
在运行时可能等同于 "darkBlue"
,但您希望编译器将它们视为不同的,因为它们具有不同的 names 或 声明站点.
相比之下,大多数 TypeScript 类型系统 structural, where two types can be seen as compatible despite having different names or declaration sites. There is a longstanding open feature request at microsoft/TypeScript#202 要求对名义类型提供一些官方支持。
如您所见,enums 是一项功能,其中有一定数量的名义打字。但是您不能以编程方式生成枚举,因此这不会直接为您工作。
您可以做的是"brand" a string literal type by intersecting它带有一些名义上的或名义上的类型。这种品牌模拟类型系统中的标称类型。
使用交集意味着品牌类型将被视为可分配给字符串文字类型,但反之则不然。 (所以 NewColors extends 'red' | 'green' | 'lightBlue' | 'darkBlue'
应该是真的,但是 'red' | 'green' | 'lightBlue' | 'darkBlue' extends NewColors
应该是假的)。
根据相交类型,您可能需要在 createKeyEnum()
的实现中使用 type assertion 来让编译器相信您确实拥有该类型的值。毕竟,实际值只是一个字符串,当我们使用字符串代替标称类型的值时,编译器会报错。因此可能需要一些错误抑制。
假设我们有一个名为 Brand<T, U>
的类型别名,它将 T
标记为某种类型,该类型也依赖于 U
(因此 Brand<"darkBlue", Colors>
和 Brand<"darkBlue", SomeOtherEnum>
将彼此不兼容)。我们可以担心下面如何定义Brand
。现在,让我们说我们拥有它。
然后你可以这样实现createKeyEnum()
:
function createKeyEnum<E>(e: E) {
class foo { private bar = 0 }
return Object.keys(e).reduce((p, v) => {
(p as any)[v] = v;
return p;
}, {} as { [K in keyof E]: Brand<K, E> }); // <-- need to assert here
}
并且因为您有意使 NewColors
与 keyof typeof Colors
在某种程度上不兼容,您可能需要在索引 Colors
时明确地将前者扩展为后者以避免错误:
function getOldColor(color: NewColors) {
const k: keyof typeof Colors = color; // widen here
return Colors[k]; // now we can use k as a key
}
这会给你想要的行为:
getOldColor(NewColors.darkBlue); // passes
getOldColor('darkBlue'); // fails
const color = getNewColor(Colors.darkBlue);
// const color: NewColors
那么,我们应该如何实现Brand
呢?最简单的方法是使用“幻影”属性:
type Brand<T, U> = T & { _tag: U };
这使用结构类型来模拟名义类型。没有什么能阻止某人实际添加 _tag
属性,但很难想象有人通过 getOldColor(Object.assign("darkBlue", { _tag: Colors }))
规避安全性。如果有人遇到这样的麻烦,也许你应该让他们去做。
如果你真的想阻止这种情况,你可以使用 class
和 a private
member:
declare class BrandClass<U> { private constructor(); private _tag: U };
type Brand<T, U> = T & BrandClass<U>
由于 _tag
成员是 private
,编译器将 BrandClass
类型视为标称类型,因此不使用名称就无法获得 BrandClass
实例BrandClass
:
getOldColor(Object.assign("darkBlue", { _tag: Colors })); // error
而且你实际上不能构造它,因为构造函数也是 private
:
getOldColor(Object.assign("darkBlue", new BrandClass<typeof Colors>())); // error
这使得某人几乎不可能在不使用类型断言的情况下生成编译器视为 Brand<T, U>
的值。