TypeScript Interview Questions: Types, Generics, and Advanced Patterns (2025)

TypeScript has become the default choice for large-scale JavaScript projects. Interviews for frontend, full-stack, and Node.js roles routinely include TypeScript questions beyond basic syntax — interviewers want to see you navigate the type system, write reusable generics, and understand how TypeScript compiles to JavaScript.

The Type System Fundamentals

What is the difference between type and interface?

// Interface: extensible with declaration merging, better for OOP shapes
interface User {
    id: number;
    name: string;
}
interface User {        // merges with the first declaration
    email: string;      // now User requires id, name, and email
}

// Type alias: more flexible — can represent unions, intersections, primitives
type ID = string | number;
type Point = { x: number; y: number };
type Named = { name: string };
type NamedPoint = Point & Named;   // intersection — type can do this easily

// Rule of thumb:
// Use interface for object shapes that may be extended
// Use type for unions, intersections, and complex compositions

What is the difference between unknown, any, and never?

Type Assignable from Assignable to Use When
any Anything Anything Opt out of type checking (avoid)
unknown Anything Only unknown or any Safe alternative to any — forces narrowing before use
never Nothing Anything (bottom type) Unreachable code, exhaustive checks, empty unions
function processInput(input: unknown) {
    // input.toUpperCase();  // Error — unknown requires narrowing first
    if (typeof input === "string") {
        input.toUpperCase();  // OK — narrowed to string
    }
}

// never for exhaustive checks
type Shape = "circle" | "square" | "triangle";
function describe(s: Shape): string {
    switch (s) {
        case "circle":   return "round";
        case "square":   return "four sides";
        case "triangle": return "three sides";
        default:
            // If you add a new Shape and forget a case,
            // TypeScript errors here at compile time
            const exhaustive: never = s;
            throw new Error("Unhandled shape: " + exhaustive);
    }
}

Generics

Write a generic identity function

// Without generics: loses type information
function identity(x: any): any { return x; }

// With generics: preserves and propagates the type
function identity<T>(x: T): T { return x; }

const n = identity(42);      // type: number
const s = identity("hello"); // type: string

// Generic with constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user = { id: 1, name: "Alice" };
const name = getProperty(user, "name");  // type: string
// getProperty(user, "age");             // Error — "age" not in User

Generic Data Structures

class Stack<T> {
    private items: T[] = [];

    push(item: T): void { this.items.push(item); }
    pop(): T | undefined { return this.items.pop(); }
    peek(): T | undefined { return this.items[this.items.length - 1]; }
    get size(): number { return this.items.length; }
    isEmpty(): boolean { return this.items.length === 0; }
}

const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop());  // 2

// Generic Result type (Rust-inspired)
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

function parseJSON<T>(json: string): Result<T> {
    try {
        return { ok: true, value: JSON.parse(json) as T };
    } catch (e) {
        return { ok: false, error: e as Error };
    }
}

Utility Types

interface User {
    id: number;
    name: string;
    email: string;
    role: "admin" | "user";
    createdAt: Date;
}

// Partial: all fields optional
type UserPatch = Partial<User>;    // for PATCH requests

// Required: all fields required (opposite of Partial)
type FullUser = Required<UserPatch>;

// Pick: select specific fields
type UserSummary = Pick<User, "id" | "name">;

// Omit: exclude specific fields
type UserWithoutDates = Omit<User, "createdAt">;

// Record: dictionary type
type RoleMap = Record<"admin" | "user", string[]>;

// ReturnType: extract return type of a function
function getUser() { return { id: 1, name: "Alice" }; }
type GetUserReturn = ReturnType<typeof getUser>;  // { id: number; name: string }

// Parameters: extract parameter types
type GetUserParams = Parameters<typeof getUser>;  // []

// Readonly: make all fields immutable
type ImmutableUser = Readonly<User>;

Conditional Types and Template Literal Types

// Conditional types: ternary for types
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;  // true
type B = IsString<number>;  // false

// Infer: extract type from within a conditional
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Resolved = UnwrapPromise<Promise<string>>;  // string
type NotAPromise = UnwrapPromise<number>;         // number

// NonNullable: removes null and undefined from a union
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;  // string

// Template literal types (powerful for string APIs)
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

type CSSProperty = "margin" | "padding";
type CSSDirection = "Top" | "Bottom" | "Left" | "Right";
type CSSKey = `${CSSProperty}${CSSDirection}`;
// "marginTop" | "marginBottom" | ... | "paddingRight"

Declaration Merging and Module Augmentation

// Augment an existing library type
declare module "express" {
    interface Request {
        user?: { id: string; email: string };
    }
}

// Now TypeScript knows about req.user in Express handlers
app.get("/profile", (req, res) => {
    const userId = req.user?.id;  // type: string | undefined — no any!
});

// Namespace merging
namespace Validation {
    export interface StringValidator {
        isValid(s: string): boolean;
    }
}
namespace Validation {
    export class LettersValidator implements StringValidator {
        isValid(s: string) { return /^[A-Za-z]+$/.test(s); }
    }
}

Discriminated Unions (Pattern for State Machines)

type RequestState<T> =
    | { status: "idle" }
    | { status: "loading" }
    | { status: "success"; data: T }
    | { status: "error"; error: Error };

function renderUser(state: RequestState<User>) {
    switch (state.status) {
        case "idle":    return "No request yet";
        case "loading": return "Loading...";
        case "success": return state.data.name;  // data is typed as User
        case "error":   return state.error.message;
    }
}

// Pattern in Redux actions
type Action =
    | { type: "INCREMENT"; amount: number }
    | { type: "DECREMENT"; amount: number }
    | { type: "RESET" };

function reducer(state: number, action: Action): number {
    switch (action.type) {
        case "INCREMENT": return state + action.amount;
        case "DECREMENT": return state - action.amount;
        case "RESET":     return 0;
    }
}

Mapped Types

// Build a type dynamically from another type
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User { id: number; name: string; }
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; }

// Make specific fields optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type UserCreate = PartialBy<User, "id">;
// { id?: number; name: string } — useful for DB inserts before ID is assigned

Common Interview Questions

What is structural typing?

TypeScript uses structural typing (duck typing): two types are compatible if they have the same shape, regardless of name. A value of type { name: string; age: number } is assignable to { name: string } because it has all required properties. This differs from nominal typing (Java/C++) where types must share an explicit inheritance relationship.

What is the difference between as and satisfies?

// "as" overrides the type — bypasses checks
const config = { port: 3000 } as { port: string };  // no error, but wrong at runtime

// "satisfies" validates the type without widening it
const config2 = { port: 3000 } satisfies { port: string };  // Error! 3000 is not string

// Best use of satisfies: ensure a value matches a type while keeping the inferred type
const palette = {
    red: [255, 0, 0],
    blue: "#0000ff",
} satisfies Record<string, string | number[]>;
// palette.red is still number[] (not string | number[]) — inference preserved

When does TypeScript narrow types?

TypeScript narrows the type of a variable in conditional branches based on type guards:

  • typeof x === "string" — narrows to string
  • x instanceof Date — narrows to Date
  • x !== null — removes null from the union
  • "kind" in x — narrows to types that have the “kind” property
  • User-defined type guards: function isUser(x: unknown): x is User { ... }

Frequently Asked Questions

What is the difference between type and interface in TypeScript?

Both declare named types, but with key differences. Interface supports declaration merging — you can split the definition across multiple declarations and TypeScript merges them. This is especially useful for augmenting library types. Type alias is more flexible — it can represent unions, intersections, primitive aliases, and mapped types that interfaces cannot. For object shapes that will be extended or that need to augment external libraries, prefer interface. For unions, intersections, and complex type computations, use type. Both are erased at compile time and have no runtime cost. In practice, the choice is often stylistic — many teams pick one and use it consistently.

What are TypeScript generics and when should you use them?

Generics are type parameters that let you write reusable code that works across multiple types while preserving type safety. Use generics when: you have a function or class that operates on values of an unknown type but needs to preserve that type through the operation (like Array.map, Promise.then); you want to constrain a parameter to satisfy certain conditions (using extends); or you need to relate the types of multiple parameters or return values. Without generics, you would use any, losing all type safety. The constraint syntax (T extends SomeType) lets you write generic code that still has access to the properties of the constrained type.

What are discriminated unions and why are they useful in TypeScript?

A discriminated union is a union type where each member has a common literal field (the discriminant) that uniquely identifies it. TypeScript uses the discriminant to narrow the type in switch statements and if blocks, giving you type-safe access to fields specific to each variant. They model state machines, Redux actions, API responses, and any "either-or" data structure safely. The key advantage over a single type with optional fields is exhaustiveness checking — if you add a new variant and forget to handle it in a switch, TypeScript errors at compile time (when you use a never assertion on the default case). This is much safer than runtime duck-typing with optional field checks.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the difference between type and interface in TypeScript?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Both declare named types, but with key differences. Interface supports declaration merging — you can split the definition across multiple declarations and TypeScript merges them. This is especially useful for augmenting library types. Type alias is more flexible — it can represent unions, intersections, primitive aliases, and mapped types that interfaces cannot. For object shapes that will be extended or that need to augment external libraries, prefer interface. For unions, intersections, and complex type computations, use type. Both are erased at compile time and have no runtime cost. In practice, the choice is often stylistic — many teams pick one and use it consistently.”
}
},
{
“@type”: “Question”,
“name”: “What are TypeScript generics and when should you use them?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Generics are type parameters that let you write reusable code that works across multiple types while preserving type safety. Use generics when: you have a function or class that operates on values of an unknown type but needs to preserve that type through the operation (like Array.map, Promise.then); you want to constrain a parameter to satisfy certain conditions (using extends); or you need to relate the types of multiple parameters or return values. Without generics, you would use any, losing all type safety. The constraint syntax (T extends SomeType) lets you write generic code that still has access to the properties of the constrained type.”
}
},
{
“@type”: “Question”,
“name”: “What are discriminated unions and why are they useful in TypeScript?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A discriminated union is a union type where each member has a common literal field (the discriminant) that uniquely identifies it. TypeScript uses the discriminant to narrow the type in switch statements and if blocks, giving you type-safe access to fields specific to each variant. They model state machines, Redux actions, API responses, and any “either-or” data structure safely. The key advantage over a single type with optional fields is exhaustiveness checking — if you add a new variant and forget to handle it in a switch, TypeScript errors at compile time (when you use a never assertion on the default case). This is much safer than runtime duck-typing with optional field checks.”
}
}
]
}

Companies That Ask This Question

Scroll to Top