Deriving types from data in TypeScript - Phelipe Teles

Deriving types from data in TypeScript

2 min.
View source code

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:

TypeScript
const options = ['OPTION_1', 'OPTION_2'] as const
type Option = (typeof options)[number]
type Option = "OPTION_1" | "OPTION_2"

typeof operator

The heart of this pattern is the typeof operator, which gives us the type of a variable, for example:

TypeScript
const hello = "hello";
type Hello = typeof hello;
type 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?

TypeScript
let hello = "hello";
type Hello = typeof hello;
type Hello = string
 
hello = '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:

TypeScript
let hello = "hello" as const;
type Hello = typeof hello;
type Hello = "hello"
 
hello = 'Hello'
Type '"Hello"' is not assignable to type '"hello"'.2322Type '"Hello"' is not assignable to type '"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.

TypeScript
const arr = ['foo', 'bar', 'baz'] as const
type ArrayValue = (typeof arr)[number]
type ArrayValue = "foo" | "bar" | "baz"

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.

TypeScript
const arr = ['foo', 'bar', 'baz']
type ArrayValue = (typeof arr)[number]
type ArrayValue = string

Deriving types from objects

Now let’s try to derive our types from an object — both getting every key or every value.

TypeScript
const obj = {
const obj: { readonly foo: 1; readonly bar: 2; readonly baz: 3; }
foo: 1,
bar: 2,
baz: 3,
} as const
 
type ObjectKey = keyof typeof obj
type ObjectKey = "foo" | "bar" | "baz"
type ObjectValue = (typeof obj)[ObjectKey]
type ObjectValue = 1 | 2 | 3

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:

TypeScript
const obj = {
const obj: { foo: number; bar: number; baz: number; }
foo: 1,
bar: 2,
baz: 3,
}
 
type ObjectKey = keyof typeof obj
type ObjectKey = "foo" | "bar" | "baz"
type ObjectValue = (typeof obj)[ObjectKey]
type ObjectValue = number

Deriving types from array of objects

We can combine both approaches to derive type from an array of objects:

TypeScript
const obj = [
{ kind: 'circle', radius: 100 },
{ kind: 'square', sideLength: 50 },
] as const
 
type Kind = typeof obj[number]['kind']
type Kind = "circle" | "square"