Deriving types from data in TypeScript
A TypeScript pattern I like to use is to derive types from data. Types in TypeScript only exist at build time, so this pattern has the benefit of types being automatically linked to the runtime values they represent.
Here’s one example of what I mean by that:
const optionsconst options: readonly ["OPTION_1", "OPTION_2"]
= ['OPTION_1', 'OPTION_2'] as consttype const = readonly ["OPTION_1", "OPTION_2"]
type Option = (typeof optionsconst options: readonly ["OPTION_1", "OPTION_2"]
)[number]
typeof
operator
The heart of this pattern is the typeof
operator,
which gives us the type of a variable, for example:
const helloconst hello: "hello"
= "hello";
type Hello = typeof helloconst hello: "hello"
;
const
vs let
The result of typeof
will differ depending on how we declare our variable —
whether we used const
or let
.
As you could see, when we used const
, the Hello
type was the literal string
"hello"
— this is because a const
variable can never be re-assigned later,
so it can only be "hello
” at runtime as well.
What happens if we use let
?
let hellolet hello: string
= "hello";
type Hello = typeof hellolet hello: string
;
hellolet hello: string
= 'Hello'
The Hello
type gets
“widened”
to the string
type — because now the variable can be re-assigned to any
other value of type string
in another part of the program!
const
assertions
We could achieve the same effect of using const
with a const
assertion:
let hellolet hello: "hello"
= "hello" as consttype const = "hello"
;
type Hello = typeof hellolet hello: "hello"
;
hello = 'Hello'
We’ll often use const
assertions to prevent type widening when deriving types
from data — i.e., avoiding a more specific (narrower) type being changed to a
more general (widened) type, such as a 0
being changed to a number
and a
"hello"
being changed to string
.
Deriving types from arrays
Suppose we have an array of strings and we want a type that is an union of the array values.
const arrconst arr: readonly ["foo", "bar", "baz"]
= ['foo', 'bar', 'baz'] as consttype const = readonly ["foo", "bar", "baz"]
type ArrayValue = (typeof arrconst arr: readonly ["foo", "bar", "baz"]
)[number]
The result of typeof arr
is just an array type, the trick here is to use
number
to index that array — it’s like saying, what are all the possible
types when I index this array by a number
?
The const
assertion is critical here — if we remove it, the type gets
widened to string
.
const arrconst arr: string[]
= ['foo', 'bar', 'baz']
type ArrayValue = (typeof arrconst arr: string[]
)[number]
Deriving types from objects
Now let’s try to derive our types from an object — both getting every key or every value.
const obj = { foofoo: 1
: 1,
barbar: 2
: 2,
bazbaz: 3
: 3,
} as consttype const = {
readonly foo: 1;
readonly bar: 2;
readonly baz: 3;
}
type ObjectKey = keyof typeof objconst obj: {
readonly foo: 1;
readonly bar: 2;
readonly baz: 3;
}
type ObjectValue = (typeof objconst obj: {
readonly foo: 1;
readonly bar: 2;
readonly baz: 3;
}
)[ObjectKeytype ObjectKey = "foo" | "bar" | "baz"
]
To get the object keys, we need to use the keyof
operator.
For the object values, we just need to index that object with all of the object possible keys — which makes sense!
The const
assertion is also critical here, because otherwise the object value
types would have been widened to number
:
const obj = { foofoo: number
: 1,
barbar: number
: 2,
bazbaz: number
: 3,
}
type ObjectKey = keyof typeof objconst obj: {
foo: number;
bar: number;
baz: number;
}
type ObjectValue = (typeof objconst obj: {
foo: number;
bar: number;
baz: number;
}
)[ObjectKeytype ObjectKey = "foo" | "bar" | "baz"
]
Deriving types from array of objects
We can combine both approaches to derive type from an array of objects:
const objconst obj: readonly [{
readonly kind: "circle";
readonly radius: 100;
}, {
readonly kind: "square";
readonly sideLength: 50;
}]
= [
{ kindkind: "circle"
: 'circle', radiusradius: 100
: 100 },
{ kindkind: "square"
: 'square', sideLengthsideLength: 50
: 50 },
] as consttype const = readonly [{
readonly kind: "circle";
readonly radius: 100;
}, {
readonly kind: "square";
readonly sideLength: 50;
}]
type Kind = typeof objconst obj: readonly [{
readonly kind: "circle";
readonly radius: 100;
}, {
readonly kind: "square";
readonly sideLength: 50;
}]
[number]['kind']