2

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.

Misha Moroshko
  • 166,356
  • 226
  • 505
  • 746
  • You can't get that from what you have, because the country objects are widened to `code: string`. – jonrsharpe Feb 10 '21 at 09:53
  • @jonrsharpe `code: string` doesn't need to be there. I just want to ensure that the `CountryCodes` type has only the codes from `countries` and no other codes. – Misha Moroshko Feb 10 '21 at 11:06
  • @MishaMoroshko Is the argument part of function? If yes, it can be done via generics, but on its own not much can be done. – Akxe Feb 13 '21 at 02:16
  • @Akxe What argument/function are you referring to? – Misha Moroshko Feb 13 '21 at 03:26
  • @MishaMoroshko if you look on my answer you can see how hawing a wrapping function can affect the result. A lot of generics can be don inside of class for example. – Akxe Feb 13 '21 at 04:42

4 Answers4

6

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";
Akxe
  • 9,694
  • 3
  • 36
  • 71
  • Is it possible to define a `codesOf` function to get: `type CountryCodes = codesOf(countriesAbc) `? I'd like to avoid the `"" | (typeof countriesAbc)[number]["code"]` repetition. – Misha Moroshko Feb 15 '21 at 23:42
  • @MishaMoroshko No, typescript at build time does not provide any functions to generate types. It has helpers but not much of use here. You can however export the type and use it wherever you want. Just import it to another file or use it down the line (in the same file). Added example for you – Akxe Feb 16 '21 at 13:15
1

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]
} | "";
  • 1
    Thanks! That's pretty much what I was looking for! One question though, what's the importance of `as P` in `P in keyof T as P`? – Misha Moroshko Feb 17 '21 at 10:29
  • 1
    @MishaMoroshko good catch - actually, it means nothing, I removed it - initially, I was thinking about key remapping implementation, here it is: `type CountryCodes = keyof { [ P in keyof T as T[P] extends { code: string } ? T[P]["code"] : never ] : T[P] } | "";`, the results are the same, but it requires TS 4.1 – Oleg Valter is with Ukraine Feb 17 '21 at 10:39
0

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" }
];
Alexander Pavlov
  • 2,264
  • 18
  • 25
0

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);
ynishi
  • 316
  • 2
  • 2