TypeScript¶
What experienced TypeScript engineers forget before an interview, grouped by subtopic.
Type system semantics¶
Excess property checks¶
- Only fresh object literals get excess-property errors:
const p: Point = { x: 1, y: 2, z: 3 }fails. - The same literal assigned to a variable first, then to
Point, passes — the check never runs.
any, unknown, never¶
anyturns checking off in both directions;unknownaccepts anything but must be narrowed before use.neveris the empty type — the result of exhaustive narrowing and of functions that never return.
{}, object, and index signatures¶
{}means any non-nullish value — strings and numbers included — not "empty object";Objectbehaves the same.objectis any non-primitive;Record<string, never>is a truly empty object.- With an index signature (
Record<string, T>), every key reads as present and typedTunlessnoUncheckedIndexedAccessis on.
Function assignability¶
- A function returning a value is assignable to a
void-returning function type, soarr.forEach(x => out.push(x))compiles and the result is ignored. - A function with fewer parameters is assignable to one with more: callbacks may ignore arguments (
arr.map(x => x * 2)). - Only the contextual case is relaxed: a function declared with a
voidreturn type still can't return a value.
interface vs type¶
interfacesupports declaration merging (reopening adds members) and gives clearer error messages for object shapes.typecan alias unions, tuples, and mapped/conditional types, whichinterfacecan't express.
Variance annotations¶
- Generic types are checked by structure, so variance is inferred;
in/outannotations (interface Producer<out T>, 4.7) declare contravariance/covariance explicitly — faster checks and clearer errors. - Arrays are treated covariantly (
Dog[]assignable toAnimal[]) even though writes make that unsound.
Method bivariance¶
- Under
strictFunctionTypes, function-typed properties (f: (x: T) => void) check parameters contravariantly. - Method shorthand (
f(x: T): void) stays bivariant on purpose — unsound, but keeps common override patterns compiling.
Narrowing¶
Control-flow narrowing¶
typeof,instanceof,in, equality, and truthiness checks narrow a variable inside the branch where they're provably true.- Discriminated unions: a shared literal tag (
kind) letsswitch (x.kind)narrow each case. - Exhaustiveness: in the
defaultbranch, assign the value to anevervariable; adding a new union member then fails to compile until handled.
Custom type guards¶
- A type predicate (
x is Fish) or anasserts x is Tfunction packages narrowing the compiler can't infer. - Inferred type predicates (5.5): a boolean-returning function that already narrows gets
x is Tautomatically, e.g.arr.filter(x => x !== undefined).
as vs satisfies vs !¶
x as Tis an unchecked assertion — it only rejects conversions between unrelated types (bypass:as unknown as T).satisfies T(4.9) checks without changing the inferred type;x!stripsnull/undefinedwith no runtime check.
Type-level programming¶
Conditional types¶
- Conditional types distribute over a naked type parameter:
C<A | B>=C<A> | C<B>; wrap as[T] extends [U]to disable it. inferextracts a part of a type, poweringReturnType<T>andParameters<T>.
Mapped and template literal types¶
- Mapped types (
{ [K in keyof T]: T[K] }) transform each property;asremaps keys;+/-add or stripreadonlyand?. - Template literal types build string unions:
`on${Capitalize<Event>}`.
Inference controls¶
consttype parameters (5.0) infer literal types without the caller writingas const.NoInfer<T>(5.4) excludes a position from inference, so a default argument can't widenT.
Utility types on unions¶
OmitandPickaren't distributive:Omit<A | B, 'id'>keeps only keys common to both, collapsing the union.- Write a distributive version:
type DistOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never. keyof (A | B)is only the shared keys;PartialandReadonlyare shallow.
Branded types¶
- Types are compatible by shape, not declared name (structural typing), so
UserIdandOrderIdare both plainstrings; brand them for nominal-like safety:type UserId = string & { readonly __brand: 'UserId' }. - Create values only through a validating function (
asUserId(s)); the brand has no runtime cost.
Compilation and tooling¶
Type erasure¶
- Types, interfaces, and type-only imports are erased — zero runtime footprint.
- Exceptions that emit runtime code:
enums (aconst enumis inlined instead),namespaces with values, parameter properties, and legacy decorators.
Transpile-only builds¶
- esbuild, swc, and Babel strip types file by file without type checking — fast, but they emit JS from type-invalid code.
isolatedModulesflags constructs a single-file transpiler can't handle;verbatimModuleSyntaxrequires explicitimport type, so elision needs no type analysis.
Node type stripping¶
- Node runs
.tsfiles by stripping types, on by default since Node 22.18 / 23.6. erasableSyntaxOnly(5.8) forbids syntax that can't simply be erased (enums, parameter properties, value namespaces); Node still ignores tsconfig (paths, JSX) and needs explicit.tsimport extensions.
Module resolution¶
moduleResolution: bundlermirrors bundlers (extensionless imports allowed);nodenextmirrors Node exactly, requiring file extensions in ESM.node10(the oldnode) ignorespackage.jsonexports; it is deprecated in 6.0 and removed in 7.0.
TypeScript 6.0 defaults¶
- TypeScript 6.0 (March 2026), the last JS-based release, turns
stricton by default and defaultsmoduletoesnextandtargettoes2025. typesdefaults to[]: global@typespackages are no longer auto-included — add"types": ["node"]or get "Cannot find name 'process'".- Deprecates
target: es5,moduleResolution: node10,baseUrl,outFile, and AMD/UMD/SystemJS output — errors unless"ignoreDeprecations": "6.0"; 7.0 removes them.
TypeScript 7 native compiler¶
- TypeScript 7.0 (July 2026) is the Go port of the compiler, still installed as
typescriptwith atscbinary (previews shipped astsgoin@typescript/native-preview). - Full builds are typically 8–12× faster than 6.0: parsing, emit, and type checking run in parallel (
--checkers, default 4). - It ships no compiler API yet (planned for 7.1), so tools built on the 6.0 API (Vue, Svelte, and Angular template checking) still need 6.0 alongside.
Strictness flags¶
The strict family¶
strictenablesstrictNullChecks,noImplicitAny,strictFunctionTypes,strictPropertyInitialization, and more — on by default since 6.0.- Without
strictNullChecks,null/undefinedare assignable to every type exceptnever.
Flags outside strict¶
noUncheckedIndexedAccess:arr[i]andrecord[key]becomeT | undefined.exactOptionalPropertyTypes:x?: numbermeans "absent", no longer "absent or explicitlyundefined".
Declarations and runtime features¶
Enums and alternatives¶
- Numeric enums compile to a bidirectional object (name ↔ value); string enums map one way.
const enumvalues are inlined, but break underisolatedModuleswhen imported from declaration files — many teams use string-literal unions oras constobjects instead.
Declaration files and augmentation¶
.d.tsfiles hold only types, describing JS libraries or shipping types beside compiled output.declare globaland module augmentation (declare module 'express' { interface Request { user?: User } }) extend third-party types.
Decorators¶
- Standard decorators (TC39, 5.0) have no parameter decorators and no metadata by default.
experimentalDecoratorsis the older, incompatible form still used by Angular and NestJS — check which one a codebase uses.
Typing patterns¶
Runtime validation at boundaries¶
- Types are erased:
JSON.parseandres.json()returnany, and env vars orascasts are unchecked, so data can violate its declared type at runtime. - Parse untrusted input with a schema library (Zod, Valibot, ArkType) and derive the type from the schema (
type User = z.infer<typeof User>), so the two can't drift. - Type unparsed input as
unknown, notany, so it must be validated or narrowed before use.
Overloads¶
- Several overload signatures followed by one implementation signature that callers can't see.
- Resolution picks the first matching signature top-down — list specific before general; prefer generics or a union parameter when they're precise enough.
Readonly and as const¶
readonly T[]blockspush/spliceat compile time;as constnarrows a literal to its most specific type and makes it deeplyreadonly— but not arrays/objects it only references.- Both are compile-time only — nothing is frozen at runtime.