How to read other people's types
unions, narrowing, generics, utility types
What it is
The types an AI or a library writes look harder than the ones you write yourself: angle brackets, vertical bars, unfamiliar words. They read more easily than they look — you need to know four constructs.
This article is about reading. You won't have to write types like these from scratch.
What it looks like in code
Unions and narrowing
type Result =
| { ok: true; data: string[] }
| { ok: false; error: string };
function show(r: Result): string {
if (r.ok) return `received ${r.data.length}`;
return `error: ${r.error}`;
}show({ ok: true, data: ["Oslo", "Bergen"] }) -> received 2
show({ ok: false, error: "timeout" }) -> error: timeoutIt reads like this: Result is either an object with ok: true and
data, or an object with ok: false and error.
The interesting part is what happens inside the if. After the r.ok
check the compiler knows which of the two variants it is looking at: in
the first branch there is a data and no error, in the second the other
way round. Reach for r.error in the first branch and you get a compile
error.
This is called narrowing, and it is the main reason unions are used. That "flag plus data" pair is the standard way to return "it worked or it didn't" without throwing an exception.
Ordinary checks narrow too:
| Check | What it narrows |
|---|---|
if (x === null) | removes null from the union |
typeof x === "string" | leaves strings |
Array.isArray(x) | leaves arrays |
if ("data" in r) | by the presence of a field |
Generics
function firstOr<T>(items: T[], fallback: T): T {
return items.length > 0 ? items[0]! : fallback;
}firstOr(["Oslo"], "none") -> Oslo (T = string)
firstOr([10, 20], 0) -> 10 (T = number)<T> isn't a concrete type but a placeholder: "whatever type comes in
is what comes back out". Thanks to that the function works with any data
without losing the type: the result of firstOr([10, 20], 0) is a number,
not "something".
Read generics as a template with a hole: substitute a concrete type from the
call for T and everything becomes clear.
You will meet them constantly: Array<string> (the same as string[]),
Promise<City> (a promise that will return a City),
Map<string, number>.
Utility types
Ready-made type transformations. Four of them cover almost everything:
interface City { name: string; temp: number; country: string }
type CityPatch = Partial<City>; // every field optional
type CityKey = Pick<City, "name" | "temp">; // only these
type CityNoCountry = Omit<City, "country">; // all but
type ByName = Record<string, City>; // a dictionary| Notation | Reads as |
|---|---|
Partial<T> | "all the fields of T, but optional" |
Pick<T, K> | "only the fields K from T" |
Omit<T, K> | "all the fields of T except K" |
Record<K, V> | "a dictionary: keys K, values V" |
Partial turns up most often on update functions: you can send a subset of
the fields. Record<string, City> is the same as dict[str, City] in
Python.
What else you will meet
unknown — "the type is unknown, and until you check it you can do
nothing with it". The right replacement for any: it forces a check.
as const — "this value won't change"; it turns "paid" from just a
string into that exact value.
keyof City — a union of the field names: "name" | "temp" | "country".
What to tell the AI
Explain this type piece by piece, as if I were seeing
generics for the first time. What gets substituted in and
what comes out.Rewrite this function so it returns
{ ok: true, data } | { ok: false, error } instead of throwing
an exception.There are three nearly identical interfaces here. Can they be
expressed with Pick/Omit from one base? Show how.What to look for
A union with no narrowing. If the type is A | B and there isn't a
single check in the code, the compiler was silenced with an as.
A generic that is any everywhere. <T = any> switches off all the
benefit.
Duplicated types. Three interfaces with overlapping fields instead of
Pick/Omit from one. They will drift apart at the first edit.
unknown that is immediately cast with as. The point of unknown is
to check, not to get around.
A type that is too clever. If a type runs ten lines and nobody can explain it, it is harder than the problem. An AI sometimes gets carried away.
Cheat sheet
A | Bis one or the other; a check narrows it to one.The pair
{ ok: true, data } | { ok: false, error }is the standard "it worked or it didn't".<T>is a placeholder: whatever type came in is what comes out.Partialoptional,Pickonly these,Omitall but,Recorda dictionary.unknownis the honest replacement forany: it forces a check.Don't understand a type? Hover over it; the editor shows the expanded form.