Tackling the TypeScript Type Challenges
After several years of using TypeScript, I tried the TypeScript type challenges and quickly realised just how deep and expressive the type system really is. Many of the challenges left me stuck for hours. That experience pushed me to study TypeScript’s type system more formally.
This article is a collection of notes from that study—a practical overview of the concepts that repeatedly appear in the challenges. Hopefully it serves both as a reference for myself and as a guide for others working through similar problems.
Type System: Structural Typing
Before diving into specific features, it’s important to understand TypeScript’s typing model.
There are two broad families of type systems:
- Nominal typing (Java, C#): Types are distinct based on their names. Even if two types have the same structure, they are incompatible unless one is explicitly declared as the other.
- Structural typing (TypeScript, Go): Types are compatible if their shape matches. This is also known as “duck typing”.
TypeScript is structurally typed, meaning two independently declared types can be assignable if their structure aligns.
type Cat = { name: string };
type Dog = { name: string };
const pet: Cat = { name: "Fluffy" };
const pup: Dog = pet; // No error — shapes match.
This idea sits beneath nearly every advanced TypeScript feature.
Conditional Types
Conditional types allow you to express logic at the type level:
A extends B ? X : Y
Meaning: “If A is assignable to B, return X; otherwise return Y.”
type StringIfNumber<T> = T extends number ? string : number;
type A = StringIfNumber<number>; // string
type B = StringIfNumber<boolean>; // number
Readonly Arrays (Tuples)
TypeScript has tuple types, represented with fixed-length array syntax:
type T = [number, string];
At runtime, tuples are usually declared using as const, which gives you a readonly tuple:
const tuple = [0, 1, 2] as const;
// type is readonly [0, 1, 2]
When writing generic types, you must account for readonly tuples:
type List<T extends readonly any[]> = T;
type A = List<typeof tuple>; // okay
type B = List<[1, 2, 3]>; // okay
type C = List<["hello", "world"]>; // okay
The infer Keyword
infer is one of the most powerful TypeScript features. It lets you extract a type variable from within another type—but only inside a conditional type.
Extracting a resolved value
type Awaited<T> = T extends PromiseLike<infer R> ? R : T;
type A = Awaited<Promise<number>>; // number
type B = Awaited<Promise<string>>; // string
Extracting a function’s return type
type GetReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type A = GetReturn<() => "hello">; // string
Extracting rest elements
type Rest<T> = T extends [any, ...infer R] ? R : never;
type X = Rest<[1, 2, 3, 4]>; // [2, 3, 4]
Extracting function parameters
type GetArgs<T extends (...args: any) => any> = T extends (
...args: infer A
) => any
? A
: never;
type X = GetArgs<(a: string, b: number) => void>; // [string, number]
type Y = X[number]; // string | number
Multiple infer bindings
type FirstOrRest<T extends any[], P extends boolean> = T extends [
infer First,
...infer Rest,
]
? P extends true
? First
: Rest
: never;
type A = FirstOrRest<["one", "two", "three"], true>; // "one"
type B = FirstOrRest<["one", "two", "three"], false>; // ["two", "three"]
Distributive Conditional Types
When a conditional type operates on a union, it distributes over each member.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
// string[] | number[]
Another example:
type Subtract<A, B> = A extends B ? never : A;
type X = Subtract<string | number | boolean | null, number | boolean>;
// string | null
Recursive Types
Types can call themselves.
type Awaited<T> = T extends PromiseLike<infer R> ? Awaited<R> : T;
type A = Awaited<Promise<Promise<string>>>; // string
type B = Awaited<Promise<Promise<boolean>>>; // boolean
Spread Operator (Type-Level)
type Concat<A extends readonly any[], B extends readonly any[]> = [...A, ...B];
type X = Concat<[1, 2, 3], [4, 5, 6]>;
// [1, 2, 3, 4, 5, 6]
Rest Operator (Type-Level)
type FirstArgument<T extends Function> = T extends (
f: infer F,
...args: any[]
) => any
? F
: never;
type X = FirstArgument<(a: string, b: number) => void>; // string
Or tuple extraction:
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type X = First<[number, string, boolean]>; // number
Rest + infer:
type Rest<T extends any[]> = T extends [any, ...infer R] ? R : never;
type X = Rest<[number, string, boolean]>; // [string, boolean]
Checking for Object Types
type IsObject<T> = T extends object ? true : false;
type A = IsObject<{ hello: "world" }>; // true
type B = IsObject<["hello", "world"]>; // true
type C = IsObject<string>; // false
type D = IsObject<number>; // false
type E = IsObject<null>; // false
type F = IsObject<unknown>; // false
type G = IsObject<never>; // never
type H = IsObject<any>; // boolean
Values of an Array as a Union
type ArrayValues<T extends readonly any[]> = T[number];
type X = ArrayValues<["hello", "world"]>;
// "hello" | "world"
Alternative:
type ArrayValues2<T extends readonly any[]> = T extends readonly (infer R)[]
? R
: never;
Mapped Types
Mapped types let you iterate over keys and construct new types.
type Mapped<T> = { [K in keyof T]: T[K] };
const user = {
name: "Hyun Wook Kim",
alias: "Justin",
languages: ["Korean", "English"],
};
type X = Mapped<typeof user>;
Selecting only specific keys:
type PickKeys<T, P extends keyof T> = {
[K in P]: T[K];
};
type Y = PickKeys<typeof user, "name" | "alias">;
Accessing Properties
type GetName<T extends { name: string }> = T["name"];
```ts
type GetName<T extends { name: string }> = T["name"];
type A = GetName<{ name: "Charlie" }>; // "Charlie"
const user = { name: "Johnson" };
type B = GetName<typeof user>; // string
const user2 = { name: "Johnson" } as const;
type C = GetName<typeof user2>; // "Johnson"
keyof any
This represents all valid object key types in JavaScript:
type IndexTypes = keyof any;
// string | number | symbol
Summary
TypeScript’s type system is expressive enough that defining types often feels like programming. It supports conditional types, inference, distributive unions, recursion, tuple manipulation, mapped types, property lookup, and more.
If you’re tackling the TypeScript type challenges, mastering these foundations will make the problems much more approachable. Keep this reference handy and revisit the concepts as needed.