Misha Moroshko
Misha Moroshko

Reputation: 171321

How to create a type from an array of objects in TypeScript?

Given a countries array like this:

const countries = [
  { name: "Australia", code: "AU" },
  { name: "Japan", code: "JP" },
  { name: "Norway", code: "NO" },
  { name: "Russian Federation", code: "RU" }
];

What's the easiest way to generate the following type?

type CountryCodes = "" | "AU" | "JP" | "NO" | "RU";

Note: there is an extra empty string.

Upvotes: 2

Views: 945

Answers (4)

0Valt
0Valt

Reputation: 10345

First, of all, without a slight modification to your input data set type what you want cannot be achieved. As rightfully stated by jonsharpe's comment, the type of array members here is widened to { name: string; code: string; }. This is easily fixed with a as const assertion:

const countries = [
  { name: "Australia", code: "AU" },
  { name: "Japan", code: "JP" },
  { name: "Norway", code: "NO" },
  { name: "Russian Federation", code: "RU" }
] as const;

Now the array itself is considered a tuple, and each member's properties are made readonly as well. After that, you only need a mapped type to extract the tuple's values (usually done with T[number]), get the types of code members and build a union out of them:

type CountryCodes<T> = { 
  [ P in keyof T ] : T[P] extends { code: string } ? T[P]["code"] : never  
}[keyof T & number] | "";

Where T[P] extends { code: string } constraint ensures we can index T[P] with "code". The result is exactly what you want (note that everything is done purely in type system):

type cc = CountryCodes<typeof countries>; //type cc = "" | "AU" | "JP" | "NO" | "RU"

Playground


A more concise version utilizing 4.1's key remapping feature:

type CountryCodes<T> = keyof { 
  [ P in keyof T as T[P] extends { code: string } ? T[P]["code"] : never ] : T[P]
} | "";

Upvotes: 1

Akxe
Akxe

Reputation: 11485

Although the function is useless from the JS point of view. It is able to unify codes into one type.

function createCountries<T extends string>(
  contries: { name: string; code: T }[],
): { name: string; code: T }[] {
  return contries;
}

const countries = createCountries([
  { name: "Australia", code: "AU" },
  { name: "Japan", code: "JP" },
  { name: "Norway", code: "NO" },
  { name: "Russian Federation", code: "RU" }
]);

type CountryCodes = "" | (typeof countries)[number]["code"]; // "" | "AU" | "JP" | "NO" | "RU";

// Example - How to use the type
function getCountryByCode(code: CountryCode): Country | undefined {
  return countries.find(country => country.code == code);
}

Type of countries:

{
    name: string;
    code: "AU" | "JP" | "NO" | "RU";
}[]

Without further info, this is the best you can do...

type Countries = { name: string; code: string }[];

const countriesAbc = [
  { name: "Australia", code: "AU" },
  { name: "Japan", code: "JP" },
  { name: "Norway", code: "NO" },
  { name: "Russian Federation", code: "RU" }
] as const;

const countries: Countries = [...countriesAbc];

type CountryCodes = "" | (typeof countriesAbc)[number]["code"]; // "" | "AU" | "JP" | "NO" | "RU";

Upvotes: 6

ynishi
ynishi

Reputation: 326

I think it from domain viewpoint, it maybe expected country code to be less change and country name to be more flexible. If your issue is just make union string type from list of map like, another way is better.

Make union type CountryCode from const list(this is useful after):

const countryCodeList = ["AU", "JP", "NO", "RU"] as const;
type CountryCode = "" | (typeof countryCodeList)[number];

Define Country with CountryCode:

type Country = { name: string; code: CountryCode};

When use them with in, it is safe and flexible:

// mock of date access.
const fetchByCode = (code: CountryCode, lang: string) : string => {
  return "name of " + code;
};

// create county data from code list.
const createCountries = () : Country[] => {
  return countryCodeList.map((code) => {
    const name = fetchByCode(code,'en');
    return {name, code}
  });
};

// use country data.
const countriesEn = createCountries()
console.log(countriesEn);

Upvotes: 0

Alexander Pavlov
Alexander Pavlov

Reputation: 2210

You can do it slightly other way

type CountryCodes = "" | "AU" | "JP" | "NO" | "RU";

interface Country {name: string; code: CountryCodes} 
// Alternatively
// type Country = {name: string; code: CountryCodes}


const countries: Array<Country> = [ 
  { name: "Australia", code: "DE" }, // COMPILE ERROR
  { name: "Japan", code: "JP" },
  { name: "Norway", code: "NO" },
  { name: "Russian Federation", code: "RU" }
];

or even better

type NotEmptyCountryCodes = "AU" | "JP" | "NO" | "RU";

type CountryCodes = "" | NotEmptyCountryCodes;

interface Country {name: string; code: NotEmptyCountryCodes}
// Alternatively
// type Country = {name: string; code: NotEmptyCountryCodes}

const countries: Array<Country> = [ 
  { name: "Australia", code: "DE" }, // COMPILE ERROR
  { name: "Japan", code: "" }, // COMPILE ERROR
  { name: "Norway", code: "NO" },
  { name: "Russian Federation", code: "RU" }
];

Upvotes: 0

Related Questions