16 Tricky TypeScript Interview Questions for 2026

·13 min read
By ·Updated
typescriptinterview-questionsconditional-typesgenericstype-systemfrontendadvanced-typescript

This guide contains exactly 16 tricky questions and reflects stable TypeScript 6.0 in September 2026. The difficult part is not memorizing syntax; it is predicting assignability, narrowing, distribution, inference, and mutation while remembering that TypeScript types do not validate runtime data.

Table of Contents

  1. Any vs Unknown Questions
  2. Never Type Questions
  3. Infer Keyword Questions
  4. Mapped Type Modifier Questions
  5. Generic Constraint Questions
  6. Conditional Type Distribution Questions
  7. Readonly Array vs Tuple Questions
  8. Quick Reference

Any vs Unknown Questions

These questions test your understanding of TypeScript's top types and type safety.

What is the difference between any and unknown in TypeScript?

Both any and unknown can receive a value of any type. The difference is what static operations are permitted afterward. any opts out of many checks and can contaminate inferred results. unknown requires narrowing or an assertion before type-specific use.

function processValue(value: any): string {
    return value.toUpperCase();
}
 
function processValueSafely(value: unknown): string {
    return value.toUpperCase(); // Error!
}

Think of any as turning off the alarm system. Think of unknown as saying "something entered the building—verify their identity before letting them into secure areas."

unknown preserves the obligation to establish a narrower type. It does not perform runtime validation by itself; the guard or decoder must inspect the value correctly.

How do you safely use unknown values?

The safe version requires type narrowing before use. You must prove to TypeScript what the type actually is:

function processValueSafely(value: unknown): string {
    if (typeof value === 'string') {
        return value.toUpperCase(); // Now TypeScript knows it's a string
    }
    throw new Error('Expected a string');
}

When would you actually use any?

any can be unavoidable at a legacy or incorrectly typed integration boundary, inside carefully audited type-level plumbing, or during staged migration. Keep it narrow, document why unknown cannot work, add tests around the boundary, and do not expose it through a public API merely to silence an error.


Never Type Questions

These questions test your understanding of the bottom type and exhaustiveness checking.

What is the never type and when is it useful?

The never type represents an impossible value. It is the bottom type: never is assignable to every type, while ordinary values, unknown, and even any are not assignable to never.

Common uses include functions that cannot return normally, unreachable branches after narrowing, exhaustive switch checks, and filtering union members in distributive conditional types. Calling it the “opposite of any” hides any's special assignability rules; unknown is the safer top type comparison.

How do you use never for exhaustive type checks?

This pattern catches missing switch cases at compile time:

type Shape =
    | { kind: 'circle'; radius: number }
    | { kind: 'square'; side: number }
    | { kind: 'triangle'; base: number; height: number };
 
function assertNever(value: never): never {
    throw new Error('Unexpected shape: ' + JSON.stringify(value));
}
 
function getArea(shape: Shape): number {
    switch (shape.kind) {
        case 'circle':
            return Math.PI * shape.radius ** 2;
        case 'square':
            return shape.side ** 2;
        default:
            return assertNever(shape);
    }
}

This code has a bug—we forgot to handle the 'triangle' case. TypeScript catches it at compile time because after the 'circle' and 'square' cases, the only remaining possibility for shape is the triangle variant. When we try to assign this non-never value to a variable of type never, TypeScript errors: "Type triangle is not assignable to type 'never'."

If the switch is exhaustive, shape narrows to never in the default branch. Adding a union member then surfaces every incomplete switch. Untrusted runtime input still needs validation before it can safely be treated as Shape.


Infer Keyword Questions

These questions test whether you've worked with advanced conditional types.

How does the infer keyword work in conditional types?

The infer keyword lets you "capture" a type from within a conditional type. It's like a type-level variable that gets its value from pattern matching.

type MyReturnType<T> =
    T extends (...args: never[]) => infer Result ? Result : never;
type MyParameters<T> =
    T extends (...args: infer Args) => unknown ? Args : never;
 
function greet(name: string, age: number): string {
    return `Hello ${name}, you are ${age}`;
}
 
type GreetReturn = MyReturnType<typeof greet>;   // string
type GreetParams = MyParameters<typeof greet>;   // [string, number]

In T extends (...args: never[]) => infer Result ? Result : never:

  • We check if T matches the function pattern
  • If it does, Result captures the return type
  • The true branch returns that captured Result

Think of it like destructuring, but for types. Just as const { name } = user extracts the name property from an object, infer R extracts a type from within another type's structure.

How do you extract types from Promises using infer?

The infer keyword can teach the pattern, but TypeScript already ships Awaited<T> for the recursive semantics of JavaScript await:

type PromiseValue<T> =
    T extends PromiseLike<infer Value> ? Value : T;
 
type A = PromiseValue<Promise<string>>; // string
type B = Awaited<Promise<PromiseLike<number>>>; // number
type C = Awaited<null | Promise<string>>; // null | string

infer appears in the conditional pattern and introduces a type variable available in the true branch. Awaited additionally handles compatible thenables, recursive unwrapping, and null/undefined according to its documented definition.


Mapped Type Modifier Questions

These questions test understanding of mapped types and their modifiers.

What do the plus and minus signs do in mapped types?

In mapped types, you can add or remove modifiers using + and -. The + is implied when you write readonly or ?, but the - explicitly removes them.

interface User {
    readonly id: number;
    name: string;
    email?: string;
}
 
type Mutable<T> = {
    -readonly [K in keyof T]: T[K];
};
 
type Required<T> = {
    [K in keyof T]-?: T[K];
};
 
type MutableUser = Mutable<User>;
// { id: number; name: string; email?: string }
 
type RequiredUser = Required<User>;
// { readonly id: number; name: string; email: string }

-readonly strips the readonly modifier from mapped top-level properties, and -? removes their optional modifier. These are shallow type transformations: they do not freeze/unfreeze runtime objects or recursively transform nested properties.

How do you combine multiple modifiers in mapped types?

You can combine modifiers to transform multiple property characteristics at once:

type WritableRequired<T> = {
    -readonly [K in keyof T]-?: T[K];
};
 
// Makes everything mutable AND required
type FullUser = WritableRequired<User>;
// { id: number; name: string; email: string }

The modifier syntax (+readonly, -readonly, +?, -?) allows precise control over property characteristics in type transformations.


Generic Constraint Questions

These questions reveal whether you understand how generic constraints interact with type inference.

How do generic constraints preserve specific types?

The constraint K extends keyof T ensures that K can only be a valid key of T. But the return type T[K] is where the magic happens—it uses indexed access types to return the specific type of that property.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}
 
const user = { name: 'Alice', age: 30, active: true };
 
const name = getProperty(user, 'name');     // string
const age = getProperty(user, 'age');       // number
const active = getProperty(user, 'active'); // boolean
const invalid = getProperty(user, 'email'); // Error!

TypeScript tracks which specific key was passed and preserves the precise return type.

Why does indexed access typing matter for return types?

Without the generic K, you'd have to return a union of all possible property types:

function getPropertyLoose<T>(obj: T, key: keyof T): T[keyof T] {
    return obj[key];
}
 
const nameLoose = getPropertyLoose(user, 'name'); // string | number | boolean

The generic version preserves the precise type because TypeScript tracks which specific key was passed.

How do structural constraints work with generics?

Constraints are checked structurally, not nominally:

function getId<T extends { id: PropertyKey }>(value: T): T['id'] {
    return value.id;
}
 
const numeric = getId({ id: 42, name: 'Alice' }); // number
const textual = getId({ id: 'u-1', active: true }); // string

Constraints restrict admissible type arguments while inference can preserve a more specific type within those bounds. Avoid casually typing object spread as T & U: when both inputs have the same key with incompatible types, the runtime right-hand property wins while the intersection can reduce that property to never.


Conditional Type Distribution Questions

These questions catch many experienced developers off guard.

Why do conditional types distribute over unions?

Conditional types distribute over union types when the checked type is a naked type parameter. This means the conditional is applied to each union member separately:

type ToArray<T> = T extends unknown ? T[] : never;
 
type Result1 = ToArray<string>;           // string[]
type Result2 = ToArray<string | number>;  // string[] | number[]
 
// Wait, why isn't Result2 (string | number)[]?

ToArray<string | number> isn't evaluated as one check. Instead, TypeScript applies the conditional to each union member separately:

  1. ToArray<string>string[]
  2. ToArray<number>number[]
  3. Combine: string[] | number[]

How do you prevent conditional type distribution?

To prevent distribution and get (string | number)[], wrap the type parameter in a tuple:

type ToArrayNonDistributive<T> = [T] extends [unknown] ? T[] : never;
 
type Result3 = ToArrayNonDistributive<string | number>; // (string | number)[]

The brackets keep the always-true condition but prevent distribution because the checked type is no longer a naked type parameter.


Readonly Array vs Tuple Questions

These questions test understanding of TypeScript's array and tuple types.

Why can't readonly arrays be passed where mutable arrays are expected?

as const does two things: it makes the array readonly and converts it to a tuple type with literal values. TypeScript won't let you pass a readonly array where a mutable one is expected because the function could theoretically modify it.

const arr1 = [1, 2, 3];           // number[]
const arr2 = [1, 2, 3] as const;  // readonly [1, 2, 3]
 
function sum(numbers: number[]): number {
    return numbers.reduce((a, b) => a + b, 0);
}
 
sum(arr1); // Works
sum(arr2); // Error: readonly [1, 2, 3] not assignable to number[]

The issue is that number[] permits mutation through the parameter, which would violate the caller's readonly view. readonly [1, 2, 3] guarantees only that TypeScript rejects mutation through that view; it does not freeze a runtime array or prevent a separate mutable alias from changing it.

How do you fix readonly array assignment errors?

The fix is to declare the parameter as readonly:

function sum(numbers: readonly number[]): number {
    return numbers.reduce((a, b) => a + b, 0);
}
 
sum(arr1); // Works - mutable arrays are assignable to readonly
sum(arr2); // Works now

A mutable array can be viewed through a readonly parameter because the callee promises not to mutate through it, but not vice versa. An as const assertion prevents literal widening and produces a readonly tuple here; readonly input parameters accept both mutable and readonly callers.


Quick Reference

ConceptKey Points
any vs unknownany opts out of many checks; unknown requires narrowing before use
neverBottom type for exhaustive checks and impossible values
inferPattern matching to capture types in conditional types
Mapped modifiers-readonly and -? remove modifiers; + adds them
Generic constraintsK extends keyof T preserves specific key types
DistributionConditionals distribute over unions; wrap in [T] to prevent
readonly arraysCan't assign to mutable array params; use readonly T[]

Key mental models:

  • Type hierarchy: never is at the bottom (never is assignable to every type), while unknown is the safe top type (every type is assignable to it). any has special escape-hatch behavior.
  • Variance: Under strictFunctionTypes, function properties are checked contravariantly in parameter positions, with method-related compatibility caveats. Readonly arrays remove mutating methods from the static view; mutable arrays cannot safely accept a readonly view.
  • Distribution: Conditional types distribute over unions when the type parameter appears "naked" in the extends clause.
  • Never for filtering: Beyond exhaustiveness checks, never filters union members: type NonFunction<T> = T extends (...args: never[]) => unknown ? never : T removes callable members from a union.

Frequently Asked Questions

What are the most common tricky TypeScript interview questions?

Strong questions test relationships rather than syntax: any versus unknown, never as the bottom type, exhaustive discriminated unions, infer inside conditional patterns, naked-type-parameter distribution, mapped modifiers, keyof plus indexed access, structural constraints, and mutable versus readonly array views. The best answer also separates compile-time evidence from runtime validation.

What is the difference between 'any' and 'unknown' in TypeScript?

Both can receive values of any type, but any opts out of many checks and propagates through operations, while unknown requires narrowing or an assertion before type-specific use. unknown is a safer static boundary, not runtime validation by itself; the narrowing code or schema must actually inspect the value. Keep unavoidable any narrow, documented, and tested.

When would you use the 'never' type in TypeScript?

never is TypeScript's bottom type for an impossible value. It appears in functions that cannot return normally, unreachable branches after narrowing, exhaustive discriminated-union checks, and distributive conditional filters where impossible members disappear from a union. It is assignable to every type, but ordinary values, unknown, and any are not assignable to never.

How does the 'infer' keyword work in TypeScript conditional types?

infer introduces a type variable in a conditional type's matching pattern, such as T extends readonly (infer Item)[] ? Item : never. The inferred variable can be used in the true branch to extract tuple parts, parameters, return types, or nested structures. Prefer built-in utilities such as Awaited, Parameters, and ReturnType when their documented semantics fit.

What is the difference between 'readonly' and 'const' in TypeScript?

const prevents reassignment of a binding; it does not freeze the referenced object. A readonly property or readonly array blocks mutation through that static view but is usually shallow and does not freeze runtime data or prevent mutation through a mutable alias. An as const assertion prevents literal widening and produces readonly properties or tuples for that expression.

How do you create exhaustive type checks in TypeScript?

Narrow a closed discriminated union in a switch and pass the remaining value to assertNever(value: never), or use value satisfies never in an unreachable branch. Adding a union member then makes every incomplete branch fail type checking. This is compile-time exhaustiveness for the declared union, not protection against unvalidated runtime input.

Sources


Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides