Reputation: 8993
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
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