TypeScript Type System
Roughly 20% of a typical loop — and 20% of the mock exam here.
Structural typing and erasure
TypeScript compares types by structure, not by name: a value is assignable when it has the required members, whatever it was called or where it was declared. That is why a plain object literal satisfies an interface with no implements clause, and why nominal behavior has to be faked with a brand — type UserId = string & { readonly __brand: unique symbol } — plus a checked factory that produces it.
Everything type-level is erased. No interface, annotation or readonly modifier exists at runtime, so validating external data needs a real check: a hand-written guard or a schema library. An as assertion does nothing at runtime at all; it only silences the checker, which is how bad data reaches production. When the compiler refuses a direct assertion, as unknown as T is the escape hatch and the smell.
any switches checking off for that value. unknown is its safe twin: assignable from everything, assignable to nothing until narrowed. It is the right type for parsed JSON, and it is what catch (e) gives you under current defaults, because JavaScript can throw any value.
Narrowing is the daily work
The checker follows control flow. typeof x === 'number', Array.isArray(x), 'kind' in x, x instanceof Error and a truthiness check all narrow, and so does comparing a literal-typed discriminant:
type Shape = { kind: 'circle'; r: number } | { kind: 'square'; side: number };
function area(s: Shape) {
switch (s.kind) {
case 'circle': return Math.PI * s.r ** 2;
case 'square': return s.side ** 2;
default: { const _exhaustive: never = s; return _exhaustive; }
}
}
The discriminant must be a literal type — kind: string narrows nothing. The never assignment in default is the exhaustiveness check: add a variant and every switch that forgot it fails to compile.
A user-defined type predicate — function isUser(x: unknown): x is User — extends narrowing to your own checks, but the compiler trusts it without verifying the body, so a sloppy implementation is a silent hole. Since 5.5 many predicates are inferred from a straightforward function body.
Narrowing has a known blind spot: it does not reach through an unresolved generic parameter. If props: T where T extends Shape, testing props.kind narrows nothing useful — accept the union directly, or destructure into a concrete local.
Generics, constraints and inference
Constrain a parameter to what the body actually uses: function first<T extends { length: number }>(x: T). Annotating the parameter as the constraint instead would throw away the caller’s more specific type, which defeats the point of the generic.
keyof T gives the union of keys and T[K] reads a member type; together they type a generic accessor whose return type stays exact per call site. In a type position, typeof value reuses an inferred type, which pairs with as const to derive unions from data — type Mode = (typeof MODES)[number].
as const freezes inference to literal types. satisfies verifies a value against a type while each property keeps its own inferred type instead of the declared one — so with satisfies Record<Colors, string | RGB>, palette.green.toUpperCase() compiles where the annotated version gives you string | RGB. It does not preserve literals on its own; as const satisfies T is the combination that does both. Since 5.0, a const type parameter moves the as const burden from every call site into the library signature.
The utility types worth naming
Partial and Required add or remove ?; both are shallow. Pick and Omit keep or drop listed keys — and Omit does not constrain its keys to keyof T, so a typo silently removes nothing, which is how types go stale after a rename. Record<K, V> builds a lookup, and with a literal union key every member is required. ReturnType, Parameters and Awaited read a function’s shape; Awaited<ReturnType<typeof fetchUser>> is the async payload idiom.
Underneath, these are mapped and conditional types. Mapped types can add or strip modifiers — { -readonly [K in keyof T]-?: T[K] } makes everything writable and required — and infer captures a type from a matched position. A conditional type distributes over unions unless both sides are wrapped in tuples: [T] extends [U] tests the union as one unit.
Two more that show up in senior screens: strictFunctionTypes checks function-property parameters contravariantly but leaves method-syntax parameters bivariant for compatibility, and import type guarantees erasure so a single-file transpiler never emits a module import purely for a type.
Sample questions
6 of the 40 questions this domain carries in practice mode — expand one to check yourself before drilling.
1. The practical difference between any and unknown is:
- any disables checking for that value, while unknown must be narrowed before it can be used
- unknown disables checking entirely, while any still forbids calling methods on the value
- they are aliases, and unknown is simply the name preferred by the current style guides
- any is allowed only in .d.ts files, while unknown is the version meant for application code
Answer: A. unknown is assignable from everything but assignable to nothing without a check — the safe type for JSON, catch clauses and API boundaries.
2. TypeScript decides two types are compatible based on:
- their declared names, so two identically shaped interfaces are still incompatible
- the file they are declared in, since each module forms its own nominal type namespace
- their inheritance chain, requiring an explicit implements clause before assignment is allowed
- their structure — a value is assignable when it has the required members, whatever its name
Answer: D. Structural typing is why a plain object literal satisfies an interface with no implements clause — and why branding is needed for nominal behavior.
3. A discriminated union narrows inside a switch because:
- the union is rewritten into a class hierarchy during compilation, so an instanceof test applies to each case
- the compiler compares the members structurally at each case and picks the closest match
- switch statements always narrow their subject, whatever the shape of the union
- every member shares a literal-typed property, and testing it tells the checker which member this is
Answer: D. The discriminant must be a literal type — kind: 'circle', not kind: string — or the switch narrows nothing and every branch keeps the full union.
4. function isUser(x: unknown): x is User { ... } declares:
- an overload of the User constructor, usable in place of an instanceof check
- a runtime assertion that throws an error whenever the shape does not match the User interface exactly
- a type predicate — when it returns true, the checker narrows the argument to User in the caller
- a generic constraint requiring the argument to already be a User at the call site
Answer: C. The compiler trusts the predicate without verifying it, so a sloppy body is a silent hole. TypeScript 5.5 infers many predicates automatically.
5. type IsString<T> = T extends string ? true : false; IsString<string | number> evaluates to:
- boolean — the conditional distributes over the union and yields true | false
- true, because at least one member of the union extends string
- false, since the union as a whole is not assignable to string
- never, as a naked type parameter may not be tested against a primitive
Answer: A. Wrapping both sides in tuples — [T] extends [string] — turns off distribution and tests the union as one unit, which is usually what people want.
6. In type Unwrap<T> = T extends Promise<infer U> ? U : T, the infer keyword does what?
- widens U to unknown, so the true branch is always the same as the false branch
- asserts that T is a promise, producing a compile error when the check fails
- defers evaluation of the conditional until the type is used at a call site
- declares a type variable captured from the matched position, usable in the true branch
Answer: D. The same pattern backs the built-ins: ReturnType infers from the return position, Parameters from the parameter tuple, Awaited recursively.
Drill this domain in practice mode →
Independent community study resource — not affiliated with or endorsed by Oracle, Microsoft or Ecma International. JavaScript is a trademark of Oracle Corporation; TypeScript is a trademark of Microsoft Corporation. All questions and study notes are original, written from MDN, the ECMAScript specification and the TypeScript handbook. Everything runs in your browser; nothing you answer is stored or transmitted.