7+ Tricky TypeScript Interview Questions 2025: Never, Infer & Conditional Types

·11 min read
typescriptinterview-questionsconditional-typesgenericstype-systemfrontendadvanced-typescript

TypeScript adoption has grown to 78% among JavaScript developers, yet the language's most powerful features—never, infer, conditional types—remain a mystery to most. In technical screens, candidates can typically explain what interface and type do, but fewer than 30% can explain when the never type is actually useful.

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 accept any value. The difference is what you can do afterward. With any, TypeScript throws up its hands and says "do whatever you want—I'm not checking." With unknown, TypeScript says "I don't know what this is, so prove it before you use it."

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."

The unknown type was added in TypeScript 3.0 specifically to address the type safety issues with any. It's the type-safe counterpart that preserves type checking by requiring narrowing.

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?

The any type has legitimate uses—migrating JavaScript codebases, dealing with truly dynamic data where exhaustive type guards aren't practical, or temporarily bypassing type errors during prototyping. But the key is treating it as a last resort, not a default.


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 the impossible—a value that can never exist. It's the "bottom type" in TypeScript's type hierarchy, meaning nothing can be assigned to it except another never.

Common uses include functions that always throw errors, infinite loops, exhaustive switch statement checks, and filtering types in conditional types. It's the opposite of any.

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 getArea(shape: Shape): number {
    switch (shape.kind) {
        case 'circle':
            return Math.PI * shape.radius ** 2;
        case 'square':
            return shape.side ** 2;
        default:
            const exhaustiveCheck: never = shape;
            throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
    }
}

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 our switch was truly exhaustive, shape would have no possible values left in the default case, and assigning "nothing" to never is fine. This pattern is particularly valuable when unions grow over time—adding a new shape variant to the union will immediately surface any switch statements that need updating.


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 ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
 
function greet(name: string, age: number): string {
    return `Hello ${name}, you are ${age}`;
}
 
type GreetReturn = ReturnType<typeof greet>;     // string
type GreetParams = Parameters<typeof greet>;     // [string, number]

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

  • We check if T matches the function pattern
  • If it does, R captures whatever the return type is
  • We then return that captured R

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 extract the element type from a Promise:

type Awaited<T> = T extends Promise<infer U> ? U : T;
 
type A = Awaited<Promise<string>>;        // string
type B = Awaited<Promise<Promise<number>>; // Promise<number>
 
// Recursive version for nested promises
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type C = DeepAwaited<Promise<Promise<number>>>; // number

The infer keyword can only appear in the "extends" clause of a conditional type and introduces a type variable that's only in scope within the "true" branch.


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 }

So -readonly strips the readonly modifier from all properties, and -? makes optional properties required. This is how TypeScript's built-in Required<T> utility type works.

How do you combine multiple modifiers in mapped types?

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

type Writeable<T> = {
    -readonly [K in keyof T]-?: T[K];
};
 
// Makes everything mutable AND required
type FullUser = Writeable<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 merge<T extends object, U extends object>(a: T, b: U): T & U {
    return { ...a, ...b };
}
 
// Works fine
const result = merge({ name: 'Alice' }, { age: 30 });
 
// Also works - constraints are structural
const withArray = merge({ items: [1, 2, 3] }, { count: 3 });

Constraints restrict what can be passed but don't affect inference within those bounds.


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 any ? 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 [any] ? T[] : never;
 
type Result3 = ToArrayNonDistributive<string | number>; // (string | number)[]

The brackets don't change the condition—[T] extends [any] is still always true. But wrapping in a tuple prevents the distribution behavior because the type parameter is no longer "naked" in the extends clause.


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[] implies the array could be mutated (you could push, pop, etc.), but readonly [1, 2, 3] guarantees immutability.

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 passed where a readonly one is expected (widening is safe), but not vice versa. The as const assertion creates the most specific type possible—both immutable and tuple-typed—and readonly parameters are more flexible for function signatures.


Quick Reference

ConceptKey Points
any vs unknownany disables type checking; 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 (nothing assignable to it), unknown is at the top (everything assignable to it). Everything else sits in between based on structural compatibility.
  • Variance: Function parameters are contravariant, return types are covariant. This is why readonly arrays aren't assignable to mutable array parameters.
  • Distribution: Conditional types distribute over unions when the type parameter appears "naked" in the extends clause.
  • Never for filtering: Beyond exhaustiveness checks, never is useful in conditional types for filtering: type NonFunction<T> = T extends Function ? never : T removes function types from unions.


If you found this helpful, check out these related guides:


Ready to ace your interview?

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

View PDF Guides