TypeScript interviews focus on the type system's expressive features and how they prevent real bugs. These questions cover the concepts that come up most in frontend and Node.js interviews.
Both can describe object shapes, and in most everyday cases are interchangeable. Interfaces support declaration merging (multiple declarations with the same name combine) and are generally preferred for public API shapes meant to be extended or implemented by classes. Type aliases can represent unions, intersections, tuples, and mapped/conditional types that interfaces can't express, so they're the better choice for more complex or non-object type compositions. A useful rule of thumb: reach for interface when modeling a single extensible object shape, and type for anything involving unions or derived/transformed types.
'any' opts a value entirely out of type checking — you can call any method or access any property on it without the compiler complaining, which defeats the purpose of using TypeScript. 'unknown' also accepts any value, but the compiler forces you to narrow its type (via a type guard, typeof check, or assertion) before you can operate on it, making it the safer choice when a value's type genuinely isn't known upfront. A common example is a catch block or a JSON.parse result — typing it as unknown forces you to validate its shape before use, rather than silently trusting it like any would.
Generics let you write functions, classes, or types that work with a placeholder type parameter instead of a fixed concrete type, preserving type safety without duplicating code for every type. For example, a generic function <T>(arr: T[]): T that returns the first array element keeps the return type tied to whatever array type was passed in, rather than widening it to 'any'. They show up everywhere in real code — API client wrappers, React hooks like useState<T>, and container/data-structure classes all rely on generics to stay reusable without losing precise typing.
Type narrowing is how TypeScript refines a broader type (like a union) to a more specific one within a conditional branch, based on runtime checks like typeof, instanceof, truthiness checks, or custom type guard functions. For example, inside `if (typeof x === 'string')`, TypeScript knows x is a string for the rest of that block, enabling string-specific methods without a cast. Custom type guards — functions with a return type like `arg is SomeType` — let you encapsulate more complex narrowing logic and reuse it across the codebase.
'strict' is a shorthand that turns on a bundle of stricter checks, including strictNullChecks (null/undefined must be explicitly handled rather than assignable to any type), noImplicitAny (disallows implicit any typing), strictFunctionTypes, strictPropertyInitialization, and a few others. Enabling it catches significantly more real bugs at compile time, and is recommended for all new projects even though it can require more upfront type annotation work. Turning it on midway through an existing loosely-typed project is usually a bigger lift, since it surfaces every previously-ignored null/undefined and implicit-any gap at once.
These are built-in generic helpers for transforming existing types without rewriting them. Partial<T> makes all properties optional (useful for update/patch payloads); Pick<T, K> creates a type with only a subset of keys; Omit<T, K> creates a type excluding specific keys. They keep derived types in sync with a single source-of-truth type instead of manually duplicating and maintaining parallel interfaces, so if the base type changes, the derived types stay correct automatically.
Enums generate actual runtime JavaScript objects/code and provide a named, iterable set of constants (numeric or string-backed). One notable exception is `const enum`, which is fully inlined and erased by the compiler at build time rather than producing a runtime object — though it comes with caveats around tools like Babel or the isolatedModules flag that don't do full cross-file type-checking. Union string literal types (e.g., type Status = 'active' | 'inactive') exist only at compile time, add zero runtime footprint, and generally play more naturally with plain JSON/string data — this is why many style guides now prefer string literal unions over enums for simple fixed-value sets.
keyof takes an object type and produces a union of its property names as string (or number/symbol) literal types. For example, given interface User { id: number; name: string }, keyof User evaluates to 'id' | 'name'. It's commonly combined with generics to write functions that operate safely on a specific key of an object, like function getProp<T, K extends keyof T>(obj: T, key: K): T[K], which guarantees the key actually exists on the object and infers the correct return type. This pattern shows up constantly in utility libraries and state-management helpers that need to read or update arbitrary properties safely.
Mapped types let you build a new type by iterating over the keys of an existing type and transforming each property, using syntax like { [K in keyof T]: SomeTransform<T[K]> }. Partial, Readonly, Pick, and Record are all mapped types under the hood, just built in as standard utilities. You can also write your own — for example a Nullable<T> type that makes every property T[K] | null — or use modifiers like -readonly and -? to strip readonly or optional flags off an existing type. They're the underlying mechanism that makes most of TypeScript's utility-type machinery possible.
Conditional types let a type depend on a condition, using the form T extends U ? X : Y, evaluated at the type level rather than at runtime. A common use is extracting or transforming types based on structure, like type Unwrap<T> = T extends Promise<infer U> ? U : T, which pulls the resolved type out of a Promise using the infer keyword. When the checked type is a union, conditional types distribute over each member automatically (called distributive conditional types), which is how built-ins like Exclude and Extract are implemented. They're an advanced feature, but understanding them helps when reading library type definitions or debugging confusing inferred types.
'as const' is a const assertion that tells TypeScript to infer the narrowest possible, read-only type for a value instead of widening it to a general type. For example, const status = 'active' as const gives the literal type 'active' rather than the widened string, and applying it to an array or object makes every element/property readonly while keeping literal types instead of widening them. It's especially useful for defining fixed config objects, route tables, or arrays you also want to derive a union type from, using typeof combined with indexed access, like type Status = typeof statuses[number].
A discriminated union is a union of object types that all share a common literal property (the 'discriminant' or 'tag'), which TypeScript uses to narrow the type automatically inside conditionals. For example, type Shape = { kind: 'circle', radius: number } | { kind: 'square', side: number }, and checking if (shape.kind === 'circle') narrows shape to the circle variant, giving safe access to radius without a cast. This pattern is extremely common for modeling API response variants, Redux-style actions, or state machines, and pairs well with an exhaustiveness check in a switch's default case (typed as never) so the compiler flags any variant you forget to handle.
never represents a value that can never occur — it's the type of a function that always throws or never returns (like an infinite loop), and it's also what you get when narrowing has eliminated every possibility from a union. It's most practically used for exhaustiveness checking: in a switch over a discriminated union, assigning the value in the default case to a variable typed never causes a compile error if a new variant is later added and left unhandled, catching missing cases early rather than at runtime. It's different from void, which represents a function that returns normally but produces no meaningful value.
TypeScript uses structural typing, meaning two types are considered compatible if their shapes match, regardless of their declared names or where they came from — often described as 'duck typing' at the type level. So an object literal satisfies an interface as long as it has the required properties, even if it was never explicitly declared as that interface. Nominal typing, used by languages like Java or C#, instead requires an explicit declaration that a type implements or extends another, so structurally identical types remain incompatible unless formally related. TypeScript can simulate nominal typing when needed, typically via a private 'brand' property on a type, but it's a workaround rather than a built-in feature.
A generic constraint restricts what types can be passed for a type parameter, written as <T extends SomeType>, so the compiler only accepts types that satisfy that shape. Without a constraint, a generic function like function getLength<T>(item: T) can't safely access item.length, because T could be anything; adding <T extends { length: number }> guarantees that property exists. Constraints are also how the keyof-based key-access pattern works, e.g., <T, K extends keyof T>, which limits K to actual keys of T rather than any arbitrary string. This is a different use of 'extends' from conditional types, where it tests a condition rather than restricting a type parameter.
readonly marks a property so it can only be assigned when the object is first created, and any later reassignment is flagged as a compile error — it's a compile-time-only check with no runtime enforcement, so the underlying value could still be mutated via a type assertion or from plain JavaScript. It applies to interface/type properties and class fields, and when combined with arrays, ReadonlyArray<T> (or readonly T[]) prevents push, pop, splice, and index-based mutation. It's commonly used for component props or values coming from external state that shouldn't be mutated locally, catching accidental mutation bugs at compile time rather than relying on discipline alone.
An index signature lets you type an object whose exact property names aren't known ahead of time but whose value types follow a consistent pattern, written as { [key: string]: SomeType }. This is useful for things like a dictionary of counts or a lookup table keyed by dynamic IDs, where listing every key explicitly isn't practical. Index signatures can be combined with specific known properties too, as long as those properties are compatible with the index signature's value type. Record<K, V> is effectively a built-in shorthand for this common pattern.
Function overloads let you declare multiple call signatures for the same function name, each specifying different parameter and return types, followed by a single implementation signature broad enough to cover all the declared cases. Callers only see the specific overload signatures, so TypeScript picks the matching one and gives an accurate return type based on the arguments passed, rather than a broader union type. A common example is a function that returns a string when given a string input and a number when given a number input — overloads let each call site get the precise type instead of a union it would then have to narrow. They're most useful when a function's return type genuinely depends on the shape of its input in a way plain generics can't express cleanly.
A .d.ts file contains only type information — interfaces, type aliases, and function/variable signatures — with no actual implementation code, letting TypeScript type-check code that consumes a library without needing its source. Most published npm packages either ship their own .d.ts files, built from their TypeScript source, or rely on community-maintained types from the DefinitelyTyped repository, installed as @types/package-name. You'd write your own .d.ts file when integrating a plain JavaScript library that has no types, or to describe ambient globals injected outside the normal module system. They're what let editors provide autocomplete and type-checking even for third-party or non-TypeScript code.
The non-null assertion operator, written as a trailing !, tells the compiler 'trust me, this value is not null or undefined here,' suppressing the type error without adding any actual runtime check. For example, document.getElementById('root')! asserts the result isn't null even though the DOM API's return type includes null. It's risky because if the assumption is wrong at runtime, you get a regular runtime error in a spot the compiler was told to treat as safe, effectively reintroducing the exact bug TypeScript's null checking exists to prevent. It's best reserved for cases where you have information the compiler genuinely can't infer, rather than as a quick fix to silence errors.