在Typescript中从数组值创建字符串枚举

3
我有一个包含国家ISO代码的字符串数组。
const countries = ['AF', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', ...]

我想创建一个字符串枚举,其中左右两边是相同的,数组的值如下:
enum Country {
  AF = 'AF',
  AL = 'AL',
  DZ = 'DZ',
  AS = 'AS',
  AD = 'AD',
  AO = 'AO',
  AI = 'AI',
  AQ = 'AQ',
  AG = 'AG',
...
}

我不知道怎么做。提前谢谢。

谢谢 @jcal。我需要一个枚举类型来与zod库一起使用,但是在阅读文档时我意识到zod有一个z.enum()方法,可以使用我已经有的数组(而不是使用z.nativeEnum()方法)。但是我真的很希望能够得到关于你回答的更详细的解释。 - biorubenfs
确实,这两个答案都非常相似且有帮助,但你的包括一个更像枚举对象的部分,也就是说你可以使用点符号访问每个允许的值,就像在真正的枚举中一样。谢谢你的时间和耐心。 - biorubenfs
3个回答

2
如果countries数组是静态的,您可以使用以下方法将其作为字符串联合类型访问值。尽管它在运行时不像枚举那样工作(TypeScript枚举被编译为JavaScript对象,而联合类型则被编译器剥离),但它提供了开发/编译时的类型检查。
const countries = ['AF', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG'] as const;

type Country = (typeof countries)[number];

const testOk: Country = "AD";

看看这个TypeScript Playground

2
TypeScript的枚举无法通过其他TypeScript代码以编程方式生成(无论如何,都需要使用某种源代码生成工具)。但是,您可以使用带有字符串/数字字面类型的普通对象来模拟枚举的大部分功能,正如TypeScript文档中所描述的那样。

所以,如果你从数组字面量开始,并且希望编译器跟踪元素的特定文字类型,那么你应该使用const断言来要求这样做,否则编译器会推断string[],允许你添加/删除/重新排序元素。你不想添加/删除/重新排序元素,所以as const表示你将数组字面量视为不可变的,因此它始终具有已知的长度、已知的元素和已知的顺序:

const countries = ['AF', 'AL', 'DZ', 'AS', 'AD', 'AO', 
  'AI', 'AQ', 'AG', /*...and so on*/] as const;

// const countries: readonly ["AF", "AL", "DZ", "AS", "AD", "AO", 
//   "AI", "AQ", "AG" , /*...and so on*/]

现在你已经有足够的信息可以继续了。
在TypeScript中,枚举(Enums)同时为一个命名的类型和一个命名的增加了定义。 类型是枚举值的类型,意味着它是所有可能枚举值类型的联合。因此,我们可以通过使用typeof countries(使用typeof类型查询操作符获取其类型)来创建该类型,并用number进行索引(通过索引访问),因为如果你有一个数组并用某个数值作为索引键,则会得到相应的元素:
type Country = (typeof countries)[number];
// type Country = "AF" | "AL" | "DZ" | "AS" | "AD" | "AO" | 
// "AI" | "AQ" | "AG" /*...and so on*/
< p > 是你实际索引的对象,通过键来获取枚举值。在你的情况下,你希望键和值是相同的,所以我们可以使用 countries 数组来生成这样一个对象,并在类型系统中描述它的类型:

const Country = Object.fromEntries(
  countries.map(v => [v, v])
) as { [K in Country]: K };

这里使用 Object.fromEntries() 方法从键值对数组组装一个对象,是通过将 countries 的每个元素映射map到出现两次的一对中得到的。

至于类型,这是基于 Country 类型(即预期枚举值的联合)的映射类型,其中键是每个键 K,值是相同类型 K

请注意,无论是map()还是Object.fromEntries()都没有足够强的类型推断能力,以便让TypeScript编译器推断出生成的值是否符合正确的形状,因此我使用了type assertion来表示该值可以被视为该类型。
让我们同时看一下值和类型:
/* const Country: {
    AF: "AF"; AL: "AL"; DZ: "DZ"; AS: "AS";
    AD: "AD"; AO: "AO"; AI: "AI"; AQ: "AQ";
    AG: "AG"; ...and so on
} */
console.log(Country)
/* {
  "AF": "AF", "AL": "AL", "DZ": "DZ", "AS": "AS",
  "AD": "AD", "AO": "AO", "AI": "AI", "AQ": "AQ",
  "AG": "AG"  ...and so on
}  */

看起来不错!

现在你可以将 Country 当作 TypeScript 枚举类型来使用:

console.log(Country.AG) // "AG"
const c: Country = Country.AL;

请注意,这与枚举并不完全相同。
实际上,枚举会创建一个完整的命名空间,其中枚举的每个键都作为类型导出,因此,例如,Country.AL也是一个引用Country.AL值类型的类型。如果你想模拟这种情况,但不能以编程方式实现。
namespace Country {
    export type AF = typeof Country.AF;
    export type AL = typeof Country.AL;
    export type DZ = typeof Country.DZ;
    export type AS = typeof Country.AS;
    export type AD = typeof Country.AD;
    export type AO = typeof Country.AO;
    export type AI = typeof Country.AI;
    export type AQ = typeof Country.AQ;
    export type AG = typeof Country.AG;
    /* ...and so on */
}

那么您可以写

const d: Country.AL = Country.AL;

但我怀疑这是否值得。

此外,enum 值类型被认为比其字面等价物更具体。使用我们模拟的枚举,我们可以编写:

const e: Country = "AI"; // okay 

但是如果你有一个真正的enum,那么这将是一个错误,因为"AI"被认为不能赋值给Country.AI,即使它们在运行时具有相同的值。这个限制将防止某人意外地传递来自不同枚举的值,比如
enum Artist {
    Picasso = "Picasso",
    DaVinci = "Da Vinci",
    Robot = "AI"
}
const f: Country = Artist.Robot; // okay

在我们的情况下是可以接受的,因为Artist.Robot可以赋值给"AI",而"AI"是一个有效的Country,但如果Country是一个enum,则会导致错误。

这些差异很小,对于许多用例来说,非enum版本实际上更可取。但你仍然应该意识到它们。

播放代码的游乐场链接


1
要在TypeScript中使用来自countries数组的值创建一个字符串枚举,可以使用数组和循环的组合来动态生成枚举定义。
const countries = ['AF', 'AL', 'DZ', 'AS', 'AD', 'AO', 'AI', 'AQ', 'AG', /*...and so on*/];

enum Country {
  // This part will be generated dynamically based on the countries array
}

countries.forEach(countryCode => {
  Country[countryCode] = countryCode;
});

// Example usage
function getCountryName(country: Country) {
  switch (country) {
    case Country.AF:
      return 'Afghanistan';
    case Country.AL:
      return 'Albania';
    case Country.DZ:
      return 'Algeria';
    // Add more cases for other countries as needed
    default:
      return 'Unknown';
  }
}

const myCountry: Country = Country.AF;
console.log(getCountryName(myCountry)); // Output: Afghanistan

你在集成开发环境(IDE)中尝试过吗?我看到这段代码有相当多的错误 - jcalz
你遇到了什么错误? - Kevin_H
我在上面链接了TypeScript Playground,一个Web IDE。如果你能够查看它,它将会准确地显示我所指的错误。如果你不能够查看,请告诉我。 - jcalz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接