Understanding TypeScript Type Inference with Column Formatters

Just a guy who loves to write code and watch anime.
Search for a command to run...

Just a guy who loves to write code and watch anime.
No comments yet. Be the first to comment.
Qualities that make outstanding builders.

Bipedal vs quadrupedal: how many legs This is about the number of legs the creature walks on. Bipedal. Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids. Quadrupedal. Four legs. Do

Intro You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another. The problem

What is Kinematics Kinematics is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inver

Intro Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more. This post is about the less obvious

Imagine you're building a data table component. You have data that looks like this:
type User = {
age: number;
name: string;
isActive: boolean;
}
For each column, you want to create a formatter function that handles the data for that column. The challenge is to have TypeScript automatically check that each formatter gets the right type of data based on the key.
// We want to be able to write this:
const ageFormatter = {
key: 'age',
formatter: (value) => `${value} years old` // TypeScript should know value is number
};
const nameFormatter = {
key: 'name',
formatter: (value) => value.toUpperCase() // TypeScript should know value is string
};
type ColumnFormatter<T> = {
key: keyof T;
formatter: (value: T[keyof T]) => void;
}
// Problem: value could be number | string | boolean
const badFormatter: ColumnFormatter<User> = {
key: 'age',
formatter: (value) => {} // value is number | string | boolean π’
};
Works because youβre now narrowing down based on the actual key with a second generic.
type ColumnFormatter<T, K extends keyof T> = {
key: K;
formatter: (value: T[K]) => void;
}
// Problem: Have to specify 'age' twice
const betterFormatter: ColumnFormatter<User, 'age'> = {
key: 'age',
formatter: (value) => {} // value is number β
but too verbose
};
// Same user type as above!
type User = {
age: number;
name: string;
isActive: boolean;
}
type ColumnFormatter<T> = {
[K in keyof T]-?: { key: K; formatter: (value: T[K]) => void }
}[keyof T];
// Perfect! TypeScript infers everything correctly
const formatter: ColumnFormatter<User> = {
key: 'age',
formatter: (value) => {} // value is number β
};
{
age: { key: 'age'; formatter: (value: number) => void }
name: { key: 'name'; formatter: (value: string) => void }
isActive: { key: 'isActive'; formatter: (value: boolean) => void }
}
[keyof T] converts it to a union type. This way, it can only be one of the objects. And based on the specific key, TypeScript knows the value.| { key: 'age'; formatter: (value: number) => void }
| { key: 'name'; formatter: (value: string) => void }
| { key: 'isActive'; formatter: (value: boolean) => void }
When you specify key: 'age', TypeScript knows that formatter must take a number
When you specify key: 'name', TypeScript knows that formatter must take a string
And so on...
const formatters: ColumnFormatter<User>[] = [
{
key: 'age',
formatter: (value) => `${value + 1} years old` // value is number β
},
{
key: 'name',
formatter: (value) => value.toUpperCase() // value is string β
},
{
key: 'isActive',
formatter: (value) => value ? 'β
' : 'β' // value is boolean β
}
];
The great thing about this solution is that TypeScript automatically ensures type safety without needing extra type parameters. Each formatter function gets the exact type it needs based on the key you choose!