如何在打字稿 3.0 中强制接口到枚举的 "implement" 键?
How to force interface to "implement" keys of enum in typescript 3.0?
假设我有一些枚举 E { A = "a", B = "b"}
。
我想强制某些接口或类型(为了可读性,我将仅提及接口)具有 E 的所有键。但是,我想分别指定每个字段的类型。因此,{ [P in E]: any }
甚至 { [P in E]: T }
都不是正确的解决方案。
例如,代码可能包含两个实现 E:
的接口
E { A = "a", B = "b"}
Interface ISomething { a: string, b: number}
Interface ISomethingElse { a: boolean, b: string}
随着 E 在开发过程中扩展,它可能会变成:
E { A = "a", B = "b", C="c"}
Interface ISomething { a: string, b: number, c: OtherType}
Interface ISomethingElse { a: boolean, b: string, c: DiffferntType}
几个小时后:
E { A = "a", C="c", D="d"}
Interface ISomething { a: string, c: ChosenType, d: CarefullyChosenType}
Interface ISomethingElse { a: boolean, c: DiffferntType, d: VeryDifferentType}
等等等等。因此,从 https://www.typescriptlang.org/docs/handbook/advanced-types.html 看来它还不受支持。有没有我遗漏的打字稿技巧?
您可以只在枚举上使用映射类型:
enum E { A = "a", B = "b"}
type AllE = { [P in E]: any }
let o: AllE = {
[E.A]: 1,
[E.B]: 2
};
let o2: AllE = {
a: 1,
b :2
}
编辑
如果想让新创建的对象保持原来的属性类型,就要用到函数。我们需要该函数来帮助推断新创建的对象文字的实际类型,同时仍将其约束为具有 E
的所有键
enum E { A = "a", B = "b" };
function createAllE<T extends Record<E, unknown>>(o: T) : T {
return o
}
let o = createAllE({
[E.A]: 1,
[E.B]: 2
}); // o is { [E.A]: number; [E.B]: number; }
let o2 = createAllE({
a: 1,
b: 2
}) // o2 is { a: number; b: number; }
let o3 = createAllE({
a: 2
}); // error
我猜你一定要写出 enum
和 interface
,然后希望 TypeScript 会警告你 interface
缺少 [=19] 中的键=](或者如果它有额外的键)?
假设你有
enum E { A = "a", B = "b", C="c"};
interface ISomething { a: string, b: number, c: OtherType};
您可以使用 conditional types 让 TypeScript 找出 ISomething
的键中是否缺少 E
的任何成分:
type KeysMissingFromISomething = Exclude<E, keyof ISomething>;
如果您在 ISomething
中没有遗漏任何密钥,则此类型应为 never
。否则,它将是 E
的值之一,如 E.C
.
您还可以让编译器确定 ISomething
是否有任何键 不是 E
的组成部分,也可以使用条件类型...虽然这更复杂,因为 you can't quite manipulate enum
s programmatically in expected ways。在这里:
type ExtraKeysInISomething = {
[K in keyof ISomething]: Extract<E, K> extends never ? K : never
}[keyof ISomething];
同样,如果您没有额外的钥匙,这将是 never
。然后,如果其中任何一个不是 never
,您可以使用 generic constraints along with default type parameters:
强制编译时错误
type VerifyISomething<
Missing extends never = KeysMissingFromISomething,
Extra extends never = ExtraKeysInISomething
> = 0;
类型 VerifyISomething
本身并不有趣(它总是 0
),但是通用参数 Missing
和 Extra
如果它们各自的默认值会给你错误值不是 never
.
让我们试试看:
enum E { A = "a", B = "b", C = "c" }
interface ISomething { a: string, b: number, c: OtherType }
type VerifyISomething<
Missing extends never = KeysMissingFromISomething,
Extra extends never = ExtraKeysInISomething
> = 0; // no error
和
enum E { A = "a", B = "b", C = "c" }
interface ISomething { a: string, b: number } // oops, missing c
type VerifyISomething<
Missing extends never = KeysMissingFromISomething, // error!
Extra extends never = ExtraKeysInISomething
> = 0; // E.C does not satisfy the constraint
和
enum E { A = "a", B = "b", C = "c" }
interface ISomething { a: string, b: number, c: OtherType, d: 1} // oops, extra d
type VerifyISomething<
Missing extends never = KeysMissingFromISomething,
Extra extends never = ExtraKeysInISomething // error!
> = 0; // type 'd' does not satisfy the constraint
所有这些都有效...但它并不漂亮。
另一种 hacky 方法是使用虚拟 class
,其唯一目的是在您没有添加正确的属性时责骂您:
enum E { A = "a", B = "b" , C = "c"};
class CSomething implements Record<E, unknown> {
a!: string;
b!: number;
c!: boolean;
}
interface ISomething extends CSomething {}
如果遗漏其中一个属性,则会出现错误:
class CSomething implements Record<E, unknown> { // error!
a!: string;
b!: number;
}
// Class 'CSomething' incorrectly implements interface 'Record<E, unknown>'.
// Property 'c' is missing in type 'CSomething'.
它不会警告您额外的属性,尽管您可能不在乎?
无论如何,希望其中一个对你有用。祝你好运。
如果您可以使用类型(而不是接口),那么使用内置的 Record
类型实际上非常简单:
enum E { A = "a", B = "b", C="c"}
type ISomething = Record<E, { a: string, b: number, c: OtherType}>
type ISomethingElse = Record<E, { a: boolean, b: string, c: DifferentType}>
这样做,如果 ISomething
或 ISomethingElse
省略枚举中的任何键,Typescript 会警告您。
如果您不想使用字符串枚举,则可以执行以下操作:
enum E {
A,
B,
C,
}
type ISomething = Record<keyof typeof E, number>;
const x: ISomething = {
A: 1,
B: 2,
C: 3,
};
使用 Record
。只需将 Enum 类型放在第一个位置即可。不要使用 keyof
或其他任何东西。
export enum MyEnum {
FIRST = 'First',
SECOND = 'Second',
}
export const DISPLAY_MAP: Record<MyEnum, string> = {
[MyEnum.FIRST]: 'First Map',
[MyEnum.SECOND]: 'Second Map',
}
如果您缺少其中一个属性,TypeScript 将会对您大吼大叫。
jcalz 上面的优秀答案(即投票最高的答案)在最新版本的 TypeScript 中不起作用,抛出以下错误:
All type parameters are unused. ts(62305)
这是因为两个泛型参数没有被使用。我们可以通过简单地使用下划线作为变量名的第一个字符来解决这个问题,如下所示:
// Make copies of the objects that we need to verify so that we can easily
// re-use the code block below
type EnumToCheck = MyEnum;
type InterfaceToCheck = MyInterface;
// Throw a compiler error if InterfaceToCheck does not match the values of EnumToCheck
// From:
type KeysMissing = Exclude<EnumToCheck, keyof InterfaceToCheck>;
type ExtraKeys = {
[K in keyof InterfaceToCheck]: Extract<EnumToCheck, K> extends never
? K
: never;
}[keyof InterfaceToCheck];
type Verify<
_Missing extends never = KeysMissing,
_Extra extends never = ExtraKeys,
> = 0;
请注意,如果您使用 ESLint,您可能需要在某些行上添加 eslint-disable-line
注释,因为:
- “验证”类型未使用
- 两个通用参数未使用
假设我有一些枚举 E { A = "a", B = "b"}
。
我想强制某些接口或类型(为了可读性,我将仅提及接口)具有 E 的所有键。但是,我想分别指定每个字段的类型。因此,{ [P in E]: any }
甚至 { [P in E]: T }
都不是正确的解决方案。
例如,代码可能包含两个实现 E:
的接口E { A = "a", B = "b"}
Interface ISomething { a: string, b: number}
Interface ISomethingElse { a: boolean, b: string}
随着 E 在开发过程中扩展,它可能会变成:
E { A = "a", B = "b", C="c"}
Interface ISomething { a: string, b: number, c: OtherType}
Interface ISomethingElse { a: boolean, b: string, c: DiffferntType}
几个小时后:
E { A = "a", C="c", D="d"}
Interface ISomething { a: string, c: ChosenType, d: CarefullyChosenType}
Interface ISomethingElse { a: boolean, c: DiffferntType, d: VeryDifferentType}
等等等等。因此,从 https://www.typescriptlang.org/docs/handbook/advanced-types.html 看来它还不受支持。有没有我遗漏的打字稿技巧?
您可以只在枚举上使用映射类型:
enum E { A = "a", B = "b"}
type AllE = { [P in E]: any }
let o: AllE = {
[E.A]: 1,
[E.B]: 2
};
let o2: AllE = {
a: 1,
b :2
}
编辑
如果想让新创建的对象保持原来的属性类型,就要用到函数。我们需要该函数来帮助推断新创建的对象文字的实际类型,同时仍将其约束为具有 E
enum E { A = "a", B = "b" };
function createAllE<T extends Record<E, unknown>>(o: T) : T {
return o
}
let o = createAllE({
[E.A]: 1,
[E.B]: 2
}); // o is { [E.A]: number; [E.B]: number; }
let o2 = createAllE({
a: 1,
b: 2
}) // o2 is { a: number; b: number; }
let o3 = createAllE({
a: 2
}); // error
我猜你一定要写出 enum
和 interface
,然后希望 TypeScript 会警告你 interface
缺少 [=19] 中的键=](或者如果它有额外的键)?
假设你有
enum E { A = "a", B = "b", C="c"};
interface ISomething { a: string, b: number, c: OtherType};
您可以使用 conditional types 让 TypeScript 找出 ISomething
的键中是否缺少 E
的任何成分:
type KeysMissingFromISomething = Exclude<E, keyof ISomething>;
如果您在 ISomething
中没有遗漏任何密钥,则此类型应为 never
。否则,它将是 E
的值之一,如 E.C
.
您还可以让编译器确定 ISomething
是否有任何键 不是 E
的组成部分,也可以使用条件类型...虽然这更复杂,因为 you can't quite manipulate enum
s programmatically in expected ways。在这里:
type ExtraKeysInISomething = {
[K in keyof ISomething]: Extract<E, K> extends never ? K : never
}[keyof ISomething];
同样,如果您没有额外的钥匙,这将是 never
。然后,如果其中任何一个不是 never
,您可以使用 generic constraints along with default type parameters:
type VerifyISomething<
Missing extends never = KeysMissingFromISomething,
Extra extends never = ExtraKeysInISomething
> = 0;
类型 VerifyISomething
本身并不有趣(它总是 0
),但是通用参数 Missing
和 Extra
如果它们各自的默认值会给你错误值不是 never
.
让我们试试看:
enum E { A = "a", B = "b", C = "c" }
interface ISomething { a: string, b: number, c: OtherType }
type VerifyISomething<
Missing extends never = KeysMissingFromISomething,
Extra extends never = ExtraKeysInISomething
> = 0; // no error
和
enum E { A = "a", B = "b", C = "c" }
interface ISomething { a: string, b: number } // oops, missing c
type VerifyISomething<
Missing extends never = KeysMissingFromISomething, // error!
Extra extends never = ExtraKeysInISomething
> = 0; // E.C does not satisfy the constraint
和
enum E { A = "a", B = "b", C = "c" }
interface ISomething { a: string, b: number, c: OtherType, d: 1} // oops, extra d
type VerifyISomething<
Missing extends never = KeysMissingFromISomething,
Extra extends never = ExtraKeysInISomething // error!
> = 0; // type 'd' does not satisfy the constraint
所有这些都有效...但它并不漂亮。
另一种 hacky 方法是使用虚拟 class
,其唯一目的是在您没有添加正确的属性时责骂您:
enum E { A = "a", B = "b" , C = "c"};
class CSomething implements Record<E, unknown> {
a!: string;
b!: number;
c!: boolean;
}
interface ISomething extends CSomething {}
如果遗漏其中一个属性,则会出现错误:
class CSomething implements Record<E, unknown> { // error!
a!: string;
b!: number;
}
// Class 'CSomething' incorrectly implements interface 'Record<E, unknown>'.
// Property 'c' is missing in type 'CSomething'.
它不会警告您额外的属性,尽管您可能不在乎?
无论如何,希望其中一个对你有用。祝你好运。
如果您可以使用类型(而不是接口),那么使用内置的 Record
类型实际上非常简单:
enum E { A = "a", B = "b", C="c"}
type ISomething = Record<E, { a: string, b: number, c: OtherType}>
type ISomethingElse = Record<E, { a: boolean, b: string, c: DifferentType}>
这样做,如果 ISomething
或 ISomethingElse
省略枚举中的任何键,Typescript 会警告您。
如果您不想使用字符串枚举,则可以执行以下操作:
enum E {
A,
B,
C,
}
type ISomething = Record<keyof typeof E, number>;
const x: ISomething = {
A: 1,
B: 2,
C: 3,
};
使用 Record
。只需将 Enum 类型放在第一个位置即可。不要使用 keyof
或其他任何东西。
export enum MyEnum {
FIRST = 'First',
SECOND = 'Second',
}
export const DISPLAY_MAP: Record<MyEnum, string> = {
[MyEnum.FIRST]: 'First Map',
[MyEnum.SECOND]: 'Second Map',
}
如果您缺少其中一个属性,TypeScript 将会对您大吼大叫。
jcalz 上面的优秀答案(即投票最高的答案)在最新版本的 TypeScript 中不起作用,抛出以下错误:
All type parameters are unused. ts(62305)
这是因为两个泛型参数没有被使用。我们可以通过简单地使用下划线作为变量名的第一个字符来解决这个问题,如下所示:
// Make copies of the objects that we need to verify so that we can easily
// re-use the code block below
type EnumToCheck = MyEnum;
type InterfaceToCheck = MyInterface;
// Throw a compiler error if InterfaceToCheck does not match the values of EnumToCheck
// From:
type KeysMissing = Exclude<EnumToCheck, keyof InterfaceToCheck>;
type ExtraKeys = {
[K in keyof InterfaceToCheck]: Extract<EnumToCheck, K> extends never
? K
: never;
}[keyof InterfaceToCheck];
type Verify<
_Missing extends never = KeysMissing,
_Extra extends never = ExtraKeys,
> = 0;
请注意,如果您使用 ESLint,您可能需要在某些行上添加 eslint-disable-line
注释,因为:
- “验证”类型未使用
- 两个通用参数未使用