ken
ken

Reputation: 8993

Getting keys from typed dictionary fails where implicit dictionary succeeds

I have the following type definition:

export const countries = {
  US: {
    name: "United States"
  },
  UK: {
    name: "United Kingdom"
  }
};

export type CountryCodes = keyof typeof countries;

With this definition CountryCodes is successfully returning a type of: "US" | "UK

However, I want to type the the property values in countries so I instead use the following type definition:

export interface IDictionary<T = any> {
  [key: string]: T;
}

export interface ICountry {
  name: string;
  population?: number;
}

export const countries: IDictionary<ICountry> = {
  US: {
    name: "United States"
  },
  UK: {
    name: "United Kingdom"
  }
};

Now, however, the CountryCode typing is defined as string | number.

How can I get the stronger typing I want on the countries structure while maintaining the CountryCodes type definition?

Upvotes: 0

Views: 35

Answers (1)

Karol Majewski
Karol Majewski

Reputation: 25790

By defining your keys upfront

This solution requires you to define string literals used as keys in your dictionary in advance. Note: if none are provided, just a string is used as the default type parameter and your IDictionary behaves just like before.

type IDictionary<V, K extends string = string> = {
  [Index in K]: V
}

type CountryCodes = "US" | "UK"

export const countries: IDictionary<ICountry, CountryCodes> = {
  US: {
    name: "United States"
  },
  UK: {
    name: "United Kingdom"
  }
};

By using an extra function call

In this solution, we create a factory function which makes sure whatever we give it meets certain criteria.

const assert = <T>() => <U extends T>(argument: U): U => argument;

Usage:

const assertCountries = assert<IDictionary<ICountry>>();

const countries = assertCountries({
  US: {
    name: "United States"
  },
  UK: {
    name: "United Kingdom"
  },
});

type CountryCodes = keyof typeof countries; // "US" | "UK"

By dropping the type definition

Sometimes it's just easier to not use a type definition and trust TypeScript to infer the type of your object literal. In this approach, we start data-first and — if need be — create types based on your data, just like you did in the beginning.

In order to get the desired type-safety, we shift the responsibility to the consumers of your data. Here, foo makes sure its argument conforms to the desired shape.

const countries = {
  US: {
    name: "United States"
  },
  UK: {
    name: "United Kingdom"
  }
};

type CountryCodes = keyof typeof countries; // "US" | "UK"

function foo<T extends IDictionary<ICountry>>(countries: T): void {
  /* ... */
}

Upvotes: 2

Related Questions