Skip to main content
5 min read
Last updated on

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.