Master these 31 carefully curated interview questions to ace your next Typescript interview.
TypeScript is a typed superset of JavaScript adding static types for better tooling, error detection, and code maintainability.
Adds: type annotations, interfaces, enums, generics, union/intersection types, type guards. Benefits: compile-time errors, IDE autocomplete, self-documenting code. Compiles to any JS version. Used by Angular, React, Vue, and most modern frameworks.
Interfaces define object shapes and support declaration merging; types support unions, intersections, and mapped types.
interface: declaration merging, extends for inheritance. type: union (A|B), intersection (A&B), mapped types, conditional types. Both describe objects. Best practice: interface for object shapes, type for unions and complex types.
Generics let you write reusable code working with multiple types while maintaining type safety, using <T> parameters.
function identity<T>(arg: T): T. Constraints: <T extends HasLength>. Common: Array<T>, Promise<T>, Map<K,V>. Utility types are generic: Partial<T>, Pick<T,K>, Omit<T,K>, Record<K,V>.
any disables type checking; unknown requires narrowing before use; never represents impossible values.
any: opt out of type system. unknown: top type, must narrow before use. never: bottom type, for functions that never return or exhaustive checks. Use unknown for API boundaries; any only for migration.
Built-in type transformers: Partial<T>, Required<T>, Pick<T,K>, Omit<T,K>, Record<K,V>, Readonly<T>, ReturnType<T>.
Partial (all optional), Required (all required), Pick (select properties), Omit (exclude), Record (key-value map), Readonly (immutable), ReturnType, Parameters, Extract, Exclude, NonNullable. Build custom utilities with mapped and conditional types.
Type guards narrow types at runtime: typeof, instanceof, in operator, or custom predicate functions with 'is' keyword.
typeof: typeof x === 'string'. instanceof: x instanceof Date. in: 'name' in obj. Custom: function isUser(obj): obj is User. Discriminated unions: shape.kind === 'circle'. Enable safe handling of union types.
Mapped types transform properties of existing types: { [K in keyof T]: NewType } — used to build utility types.
type Readonly<T> = { readonly [K in keyof T]: T[K] }. Modifiers: +/- for readonly and ?. Key remapping with 'as'. Template literal types with mapped types for derived types.
Conditional types select types based on conditions: T extends U ? X : Y, with infer for extracting types.
type IsString<T> = T extends string ? true : false. infer: type ElementType<T> = T extends Array<infer U> ? U : T. Distributive over unions. Built-in: Exclude, Extract, ReturnType use conditional types.
Template literal types create string types from combinations: type Route = `/api/${string}` for precise string validation.
Combine with unions: type Color = 'red'|'blue'; type BgColor = `bg-${Color}`. Built-in: Uppercase<T>, Lowercase<T>, Capitalize<T>. Used with mapped types for property renaming. Enables compile-time string validation.
Add tsconfig with allowJs and strict:false, rename files incrementally starting with leaf modules, gradually enable strict checks.
Steps: (1) tsconfig.json with allowJs:true, strict:false. (2) Rename .js to .ts one at a time. (3) Start with leaf modules. (4) Add types to functions. (5) Use any temporarily. (6) Create .d.ts for third-party JS. (7) Enable strict options gradually. (8) Target: zero 'any'.
Types are erased at compile time — all checking happens during development, producing clean JS with zero runtime cost.
Structural typing (duck typing) for compatibility. Compiler performs flow analysis and type inference. Output is plain JavaScript with no type information. Tradeoff: no runtime type safety. Use Zod or io-ts for runtime validation when needed.
Interfaces define object shapes and can be extended/merged; type aliases define any type including unions, intersections, and primitives.
Interfaces: can be extended (extends), merged (declaration merging), and implemented by classes. Type aliases: can define unions (A | B), intersections (A & B), mapped types, conditional types, and tuple types. Both can define object shapes. Key difference: interfaces allow declaration merging (adding properties across files); types cannot. Use interfaces for public API contracts and object shapes. Use types for unions, complex type operations, and utility types.
Generics allow creating reusable components that work with multiple types while maintaining type safety.
Syntax: function identity<T>(arg: T): T { return arg; }. T is a type parameter — replaced with actual type at usage. Constraints: <T extends HasLength> limits T to types with length property. Generic interfaces: Array<T>, Promise<T>, Map<K, V>. Generic classes: class Stack<T> { items: T[] }. Default types: <T = string>. Multiple parameters: <T, U>. Conditional types: T extends string ? A : B. Generics enable type-safe containers, utility functions, and API responses without code duplication.
Built-in utility types transform existing types: Partial makes optional, Required makes required, Pick/Omit select/exclude properties.
Partial<T>: all properties optional. Required<T>: all required. Readonly<T>: all readonly. Pick<T, K>: select specific properties. Omit<T, K>: exclude properties. Record<K, V>: object with keys K and values V. Exclude<T, U>: remove types from union. Extract<T, U>: keep types in union. NonNullable<T>: remove null/undefined. ReturnType<T>: infer function return type. Parameters<T>: tuple of function parameters. ConstructorParameters<T>: constructor parameter types. These are implemented using mapped types and conditional types internally.
Discriminated unions use a common literal property to distinguish between union members, enabling exhaustive type narrowing.
Pattern: type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number }. The 'kind' property is the discriminant. switch(shape.kind) narrows the type in each case. Exhaustive check: default: const _never: never = shape — compile error if case missed. Benefits: type-safe, exhaustive handling, IDE autocompletion. Used extensively in Redux actions, API responses, state machines. The discriminant must be a literal type (string, number, boolean). Combine with never type for exhaustiveness checking.
Type narrowing refines a broader type to a more specific one using type guards like typeof, instanceof, in, and custom predicates.
Built-in guards: typeof x === 'string' (primitive), x instanceof Date (class), 'name' in x (property check), x === null/undefined (nullish). Custom type guards: function isUser(x: any): x is User { return 'name' in x } — return type predicate. Discriminated unions: switch on literal discriminant. Assertion functions: asserts x is User throws if false. Truthiness narrowing: if (x) removes null/undefined. Control flow analysis: TypeScript tracks narrowing through if/else, switch, early returns, and logical operators.
Mapped types create new types by transforming properties of existing types using the in keyof syntax.
Syntax: type Mapped<T> = { [K in keyof T]: NewType }. Read-only: { readonly [K in keyof T]: T[K] }. Optional: { [K in keyof T]?: T[K] }. Remove modifier: { -readonly [K in keyof T]: T[K] }. Key remapping: { [K in keyof T as NewKey]: T[K] }. Template literal keys: { [K in keyof T as `get${Capitalize<K>}`]: () => T[K] }. Built-in mapped types: Partial, Required, Readonly are implemented as mapped types. Combine with conditional types for powerful transformations. Used in library type definitions extensively.
Conditional types select types based on conditions (T extends U ? X : Y); infer extracts types within conditional type branches.
Syntax: type Check<T> = T extends string ? 'yes' : 'no'. Distributive: unions are distributed — Check<string | number> becomes 'yes' | 'no'. infer keyword: type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never — R is inferred from the return type. Nested infer: extract deeply nested types. Recursive conditional types (TS 4.1+): type DeepReadonly<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> }. Used in utility types, API type inference, and complex type transformations.
Declaration files provide type information for JavaScript libraries without including implementation, enabling type checking and IDE support.
.d.ts files contain only type declarations — no runtime code. Uses: (1) Type definitions for npm packages (DefinitelyTyped/@types). (2) Ambient declarations for global variables (declare const). (3) Module augmentation: extend existing types. (4) Generated with tsc --declaration. Triple-slash directives: /// <reference types='...' /> for dependencies. declare module 'library' for untyped libraries. declare global for extending global scope. DefinitelyTyped contains 8000+ type packages. tsconfig: skipLibCheck speeds compilation by skipping .d.ts checking.
Use incremental adoption: rename .js to .ts, start with strict: false, add types gradually, enable strict checks one by one.
Strategy: (1) Add tsconfig.json with allowJs: true, strict: false. (2) Rename files one by one: .js → .ts. (3) Fix errors: add type annotations, create interfaces. (4) Use any temporarily for complex cases, then refine. (5) Enable strict checks incrementally: noImplicitAny, strictNullChecks, strictFunctionTypes. (6) Add .d.ts for untyped deps or use @types packages. (7) Converting tests: colocate type tests. (8) CI: include type checking in build. Goal: full strict mode. Timeline: weeks to months depending on size. TypeScript 5.5 introduced --isolatedDeclarations for faster declaration emit.
Define response interfaces, use generic fetch wrappers, validate at runtime with Zod/io-ts, and use discriminated unions for errors.
Approach: (1) Define interfaces: interface UserResponse { id: number; name: string }. (2) Generic fetcher: async function api<T>(url: string): Promise<T>. (3) Runtime validation: Zod schema.parse(data) validates AND infers types. (4) Error handling: type ApiResult<T> = { success: true; data: T } | { success: false; error: string }. (5) Generated types: OpenAPI → TypeScript (openapi-typescript). (6) tRPC: end-to-end type safety without code generation. (7) Never trust API responses — validate at boundaries. Use z.infer<typeof schema> to derive types from Zod schemas.
any disables type checking entirely; unknown is type-safe — you must narrow the type before using it.
any: opt-out of type system. Any operation allowed, no type checking. Contagious — spreads through code. unknown: type-safe top type. Cannot perform operations without narrowing first. Must use type guards, assertions, or typeof before accessing properties. Use unknown for: function parameters from external sources, JSON.parse results, catch clause errors (TS 4.4+). Never use any in new code — use unknown and narrow. any assignment: any → anything works. unknown assignment: unknown → only any or unknown without narrowing.
TypeScript enums create runtime objects; alternatives include const enums (inlined), string unions, and as const objects.
Numeric enums: enum Direction { Up, Down } — generates runtime object with reverse mapping. String enums: enum Color { Red = 'RED' } — no reverse mapping, more readable. const enum: enum values inlined at compile time — no runtime object (smaller bundle). Alternatives: (1) String union: type Direction = 'up' | 'down' — no runtime cost, simpler. (2) as const: const DIRECTION = { Up: 'up', Down: 'down' } as const — runtime object + type safety. Most teams prefer string unions or as const over enums for simplicity and tree-shaking.
Decorators are functions that modify classes, methods, properties, or parameters at declaration time using @decorator syntax.
Types: @classDecorator, @methodDecorator, @propertyDecorator, @parameterDecorator. Stage 3 decorators (TS 5.0+) differ from legacy experimental decorators. Class decorator receives constructor, can modify or replace class. Method decorator receives target, propertyKey, descriptor — can wrap method. Use cases: logging, authentication, validation, ORM (TypeORM @Entity, @Column), dependency injection (NestJS @Injectable). Enable: experimentalDecorators in tsconfig (legacy) or use native Stage 3 syntax. Metadata reflection with reflect-metadata for runtime type info.
Use a generic interface mapping event names to payload types, ensuring emit and on/off handlers are type-checked.
Pattern: interface Events { login: { userId: string }; logout: void }. class Emitter<T extends Record<string, any>> { on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void; emit<K extends keyof T>(event: K, payload: T[K]): void }. Benefits: autocomplete for event names, type-checked payloads, compile-time error for wrong payload types. Can use Map<string, Set<Function>> internally. Factory pattern: createEmitter<Events>() returns typed instance. Used in libraries like mitt, Valtio, and framework event systems.
Template literal types create string types from combinations of literal types using ${...} syntax, enabling pattern-based typing.
Syntax: type Greeting = `hello ${string}`. Combinations: type Color = 'red' | 'blue'; type BgColor = `bg-${Color}` → 'bg-red' | 'bg-blue'. Utility types: Uppercase, Lowercase, Capitalize, Uncapitalize. Use cases: CSS property types, routing patterns, API endpoint types, event name generation. Key remapping: { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }. Powerful with mapped types for generating getter/setter types, action types, or query builder types automatically.
satisfies validates that an expression matches a type without widening it, preserving the narrow inferred type.
Syntax: const config = { port: 3000 } satisfies Config. Unlike type annotation (const config: Config = ...), satisfies preserves literal types. Example: const colors = { primary: '#fff' } satisfies Record<string, string> — type is { primary: string } not Record<string, string>, so colors.primary is valid. Without satisfies, accessing specific keys might need assertion. Use when you want both: validation against a type AND narrow type inference. Added in TypeScript 4.9.
TypeScript resolves modules using Node or Bundler strategies, following paths in tsconfig, node_modules, and baseUrl.
Strategies: (1) node: mimics Node.js resolution — check file, then index.ts in folder, then node_modules. (2) bundler (TS 5.0+): closer to how bundlers like Vite/webpack resolve. (3) node16/nodenext: ESM resolution with .js extensions. paths: alias mapping { '@/*': ['./src/*'] }. baseUrl: root for non-relative imports. typeRoots: where to find @types packages. moduleResolution + module settings are closely linked. Common issue: paths in tsconfig don't affect runtime — need bundler alias or tsconfig-paths. TS 5.2: --verbatimModuleSyntax for explicit import types.
Branded types add a phantom property to create distinct types from the same base type, preventing accidental mixing.
Problem: type UserId = string and type OrderId = string are interchangeable. Solution: type UserId = string & { readonly __brand: 'UserId' }. Now UserId and OrderId are incompatible. Create with factory: function toUserId(id: string): UserId { return id as UserId }. Use cases: entity IDs (prevent passing userId where orderId expected), validated strings (Email, URL), units (Meters vs Feet). The brand property doesn't exist at runtime — zero overhead. Libraries: ts-brand, io-ts branded types. Prevents entire categories of bugs at compile time.
Use never type in default case of switch/if-else chains — TypeScript errors if any union member is unhandled.
Pattern: function assertNever(x: never): never { throw new Error('Unexpected: ' + x); }. In switch: default: return assertNever(action). If you add a new union member and forget to handle it, TypeScript shows compile error because the new member can't be assigned to never. Works with discriminated unions: type Action = { type: 'add' } | { type: 'remove' }. Alternative: ts-pattern library for pattern matching with .exhaustive(). This technique catches bugs at compile time when extending union types, which is common in Redux, state machines, and API handlers.
Ready to master Typescript?
Start learning with our comprehensive course and practice these questions.