5 exercises — interface vs type alias, discriminated unions with exhaustive checking, built-in utility types, type narrowing, and reading TypeScript compiler errors in plain English.
0 / 5 completed
TypeScript advanced type vocabulary reference
interface — object shape; supports declaration merging and extends
type alias — name for any type; required for unions, intersections, mapped types
discriminated union — union with a shared literal "discriminant" field for safe narrowing
type narrowing — TypeScript narrowing a union to a specific type inside a conditional
type predicate (x is T) — user-defined type guard return type
1 / 5
During a TypeScript code review, a reviewer comments: "Prefer an interface over a type alias here — this shape needs to be extendable." What is the key difference that makes the reviewer favour interface?
TypeScript interface vs. type alias — the practical distinction:
Use interface when: • You are describing the shape of an object or class • The type might need to be extended or merged later • Working with OOP patterns (class Foo implements IFoo) • Writing public API types for libraries (consumers can augment via declaration merging)
Use type alias when: • Describing a union: type Status = "pending" | "done" | "failed" • Creating a mapped type: type Partial<T> = { [K in keyof T]?: T[K] } • Creating a conditional type: type IsString<T> = T extends string ? true : false • Creating a tuple type: type Pair = [string, number]
Declaration merging (interface only): interface User { name: string; } interface User { age: number; } // TypeScript merges these — User now has both name and age
This is used extensively by: ambient module declarations, global type augmentation (e.g., extending Express Request), adding custom fields to third-party library types.
Vocabulary: • declaration merging — TypeScript automatically combines multiple declarations of the same identifier • extending an interface — interface Admin extends User { role: string; } • type alias — a named reference to any TypeScript type, including complex/utility types
2 / 5
A TypeScript PR description says: "Used a discriminated union with exhaustive never checking to make the switch statement future-proof." What does this mean?
Discriminated unions — the pattern explained:
A discriminated union is a union type where every member has a common literal property — the discriminant. TypeScript uses it to narrow types inside if / switch branches.
Example: type Shape = | { kind: "circle"; radius: number } | { kind: "square"; side: number } | { kind: "triangle"; base: number; height: number };
Here kind is the discriminant — a literal string type.
Exhaustive check with never: function area(s: Shape): number { switch (s.kind) { case "circle": return Math.PI * s.radius ** 2; case "square": return s.side ** 2; case "triangle": return 0.5 * s.base * s.height; default: const _exhaustive: never = s; // ← compile error if Shape grows return _exhaustive; } }
If someone adds | { kind: "pentagon"; ... } to the union but forgets to add a case, TypeScript errors: "Type '{ kind: "pentagon"; ... }' is not assignable to type 'never'." The compiler catches the missing case.
Vocabulary: • discriminant — the shared literal property that distinguishes union members • type narrowing — TypeScript inferring a more specific type inside a branch • exhaustive check — guaranteeing all cases are handled • never — the type with no possible values; used as the "impossible case" in exhaustive checks • future-proof — the code will produce a compile error, not a silent bug, when extended
3 / 5
A developer reads a utility type usage: type UpdateUser = Partial<Pick<User, "name" | "email" | "bio">>. How would you explain this in plain English to a junior engineer?
TypeScript's built-in utility types are composable transformations on types — each one takes a type and returns a modified type.
Core utility types:
Partial<T> — makes all properties optional (name?: string instead of name: string). Use for PATCH endpoints where any subset of fields may be updated.
Required<T> — inverse: makes all optional properties required.
Pick<T, K> — creates a type with only the specified keys from T. Pick<User, "name" | "email"> gives { name: string; email: string }.
Omit<T, K> — inverse of Pick: creates a type with all keys except the specified ones. Omit<User, "password" | "role"> gives a safe "public user" shape.
Readonly<T> — all properties become readonly.
Record<K, V> — creates an object type with keys K and values V. Record<string, number> = { [key: string]: number }.
ReturnType<T> — extracts the return type of a function type.
Parameters<T> — extracts the parameter types as a tuple.
Composing utility types: Partial<Pick<User, "name" | "email" | "bio">> reads right-to-left: 1. Pick<User, ...> — take only name, email, and bio from User 2. Partial<...> — make all of them optional
This is the TypeScript idiom for defining a "partial update" DTO (Data Transfer Object).
4 / 5
A TypeScript function signature: function processInput(val: string | number | null) { if (typeof val === "string") { val.toUpperCase(); } }. What is happening inside the if block?
Type narrowing is TypeScript's ability to refine a broad union type to a specific type within a conditional block — automatically, without a cast.
Type narrowing techniques:
1. typeof narrowing — works for primitives: typeof val === "string" → narrows to string typeof val === "number" → narrows to number typeof val === "function" → narrows to Function
2. instanceof narrowing — works for objects: if (error instanceof NetworkError) { error.statusCode }
3. in operator narrowing — checks if property exists: if ("radius" in shape) { shape.radius } → narrows to shapes with radius
5. User-defined type guard: function isString(val: unknown): val is string { return typeof val === "string"; } The val is string return type tells TypeScript: "if this function returns true, narrow the argument to string."
Type narrowing vocabulary: • narrow — reduce the set of possible types inside a branch • type guard — an expression that narrows a type; can be built-in (typeof) or user-defined • control flow analysis — TypeScript's tracking of types through branches and loops • type predicate (x is T) — the return type of a user-defined type guard function
5 / 5
A developer shares a tsc error: Type '{ name: string; age: string; }' is not assignable to type 'User'. Types of property 'age' are incompatible. Type 'string' is not assignable to type 'number'. How would you explain this error to someone new to TypeScript?
Reading TypeScript error messages — a structured approach:
The error format: Type 'A' is not assignable to type 'B' — you tried to assign something of type A where type B is expected. A is what you provided; B is what is required.
Reading the error in this exercise: 1. "Type '{ name: string; age: string; }' is not assignable to type 'User'" — you have an object literal, and TypeScript is trying to use it as a User 2. "Types of property 'age' are incompatible" — the problem is specifically the age field 3. "Type 'string' is not assignable to type 'number'" — age was given as a string ("25") but User declares it as a number (25)
Common TypeScript errors and their plain-English meaning: • "is not assignable to type" — type mismatch at assignment point • "Property X does not exist on type Y" — typo in property name, or the type doesn't have that field • "Object is possibly null" — you need to add a null check before accessing the value • "Argument of type X is not assignable to parameter of type Y" — wrong type passed to a function • "Cannot find name X" — variable/function not in scope, or missing import • "Index signature" errors — accessing an object with a dynamic key when TypeScript can't verify the key exists
Debugging mental model: TypeScript errors always point to where the type system detected a conflict. Read from the bottom of the error stack up — the innermost message is usually the actual type mismatch; outer messages are "container" context.