Typescript - Creating custom types from string literal (union) types
There may be times when you need to compose a new type from already existing string literal union type.
Composing here could be just removing some entries from the original type or adding new ones along with the removal.
What is a String literal Type ?
from TS official documentation: https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types
String literal types allow you to specify the exact value a string must have. In practice string literal types combine nicely with union types, type guards, and type aliases. You can use these features together to get enum-like behavior with strings.
TLDR; A string literal type can be considered a subtype of the string type. This means that a string literal type is assignable to a plain string, but not vice-versa.
Let’s imagine that we have a string literal type to contain a mix of colours as follows:
type Colours =
| "Red"
| "Crimson"
| "Firebrick"
| "DarkSalmon"
| "LightCoral"
| "Blue"
| "Navy"
| "MidNightBlue";
To have a separate type for the reds
alone, we would either have to duplicate or split the above type. It could look as follows:
type Reds = "Red" | "Crimson" | "Firebrick" | "DarkSalmon" | "LightCoral";
type Blues = "Blue" | "Navy" | "MidNightBlue";
type Colors = Reds | Blues;
This approach may not work when we want to modify a widely referenced type or if the type is from external library. There are also various other reasons the above approach may not work in all circumstances.
We could also play around the TS’s Mapped types to create new types effortlessly wherever needed.
Mapped string literal type
We are going to come up with a mapped type so that TS can infer the entries as index-based items. Based on that, we can create custom types using TS Pick.
export type MappedLiteral<T extends string> = {
[K in T]: boolean;
};
There are three parts:
- The type variable
K
, which gets bound to each property in turn. - The string literal union of type
T
, which contains the names of properties to iterate over. - The resulting type of the property.
The above type will only get us through halfway. for e.g. using this against the our original string literal type type Reds = MappedLiteral<Colours>
is equivalent to writing:
type Reds = MappedLiteral<Colours>;
// is equivalent to
type Reds = {
Red: boolean;
Crimson: boolean;
Firebrick: boolean;
DarkSalmon: boolean;
LightCoral: boolean;
Blue: boolean;
Navy: boolean;
MidNightBlue: boolean;
};
For us to be able to create custom types out of string literal types; we would need to have the following type along with the above type.
export type MappedStringLiteralPick<
T extends string,
K extends keyof MappedLiteral<T> = keyof MappedLiteral<T>
> = keyof Pick<MappedLiteral<T>, K>;
This uses TS’s utility type Pick
to pick the properties inferred from our custom mapped MappedLiteral
type.
Now, we can use this utility type to create custom types from string literal types. So, we can have something like below to create a specific type for reds
.
type Reds = MappedStringLiteralPick<
Colours,
"Red" | "Crimson" | "Firebrick" | "DarkSalmon" | "LightCoral"
>;
// is equivalent to
type Reds = "Red" | "Crimson" | "Firebrick" | "DarkSalmon" | "LightCoral";