Typescript · Type System

Type System Interview Questions
With Answers & Code Examples

8 carefully curated Type System interview questions with working code examples and real interview gotchas.

Practice Interactively →← All Categories
8 questions3 beginner4 core1 advanced
Q1Beginner

What is type inference in TypeScript and when does it kick in?

💡 Hint: TypeScript deduces types automatically from context — assignments, return values, default parameters

Type inference is TypeScript's ability to automatically determine a value's type without an explicit annotation.

Where inference applies:

  • Variable initialization — const x = 42 infers number
  • Function return type — inferred from the return expression
  • Default parameters — function greet(name = 'World') infers name: string
  • Destructuring — inferred from the source type
  • Generic instantiation — argument types drive the type parameter
// Variable initialization
const count = 0;       // inferred: number
const name = 'Alice';  // inferred: string
const active = true;   // inferred: boolean

// Function return
function add(a: number, b: number) {
  return a + b; // return type inferred as number
}

// Contextual typing — callback param inferred from array type
const nums = [1, 2, 3];
nums.forEach(n => console.log(n.toFixed(2))); // n inferred as number

// Generic inference
function identity<T>(value: T): T { return value; }
const result = identity('hello'); // T inferred as string
💡 Prefer inference over redundant annotations. Write const x = 42 not const x: number = 42. Annotate function parameters and public API shapes where inference can't help consumers.
Practice this question →
Q2Beginner

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

💡 Hint: any opts out of type checking; unknown is safe any (must narrow); never is the bottom type — unreachable code

These three types occupy opposite ends of the type hierarchy.

any — the escape hatch. Disables all type checking for that value. Assignable to and from everything.

let a: any = 42;
a.foo.bar.baz; // no error — type checking disabled
const b: number = a; // OK — any is assignable to anything

unknown — the type-safe alternative to any. You must narrow it before using it.

let u: unknown = getValueFromAPI();

u.toUpperCase(); // ❌ Error — must narrow first

if (typeof u === 'string') {
  u.toUpperCase(); // ✅ Safe — narrowed to string
}

// unknown is NOT assignable to specific types without narrowing
const s: string = u; // ❌ Error

never — the bottom type. A value that can never exist. Used for:

  • Functions that never return (throw or infinite loop)
  • Exhaustive checks in switch/if statements
  • Impossible intersections
function fail(msg: string): never {
  throw new Error(msg); // never returns
}

// Exhaustive check — never signals unhandled case
type Shape = 'circle' | 'square';
function area(s: Shape) {
  switch (s) {
    case 'circle': return Math.PI;
    case 'square': return 1;
    default:
      const _exhaustive: never = s; // ❌ Error if new case not handled
  }
}
💡 Prefer unknown over any for values from external sources (API responses, JSON.parse). It forces you to validate before use — the TypeScript equivalent of defensive programming.
Practice this question →
Q3Core

What are type assertions and when should you use them?

💡 Hint: as Type tells TypeScript "trust me" — use sparingly; prefer type guards for safety

Type assertions tell TypeScript to treat a value as a specific type, overriding inferred or declared types. They do NOT perform runtime conversion.

// as syntax (preferred)
const input = document.getElementById('name') as HTMLInputElement;
input.value; // now valid — TypeScript trusts your assertion

// angle-bracket syntax (avoid in .tsx files)
const input2 = <HTMLInputElement>document.getElementById('name');

// Non-null assertion operator (!) — asserts value is not null/undefined
const el = document.querySelector('.btn')!; // you assert it exists
el.click();

// Double assertion — escape hatch when types seem incompatible
const x = someValue as unknown as TargetType;

When to use:

  • Narrowing DOM types (as HTMLInputElement) where TypeScript can't know the exact element
  • After validating data from external sources where you've confirmed the shape
  • Testing utilities where you intentionally pass partial objects

When NOT to use:

  • As a shortcut to silence type errors — the error exists for a reason
  • Instead of proper type guards that provide actual runtime safety
// ❌ Silencing a real bug
const user = getUser() as User; // getUser might return null!

// ✅ Proper guard
const maybeUser = getUser();
if (maybeUser !== null) {
  const user: User = maybeUser; // safe
}
💡 Each as assertion is a promise you make to TypeScript. Every assertion you write is a potential source of runtime errors if your assumption is wrong.
Practice this question →
Q4Beginner

What is structural typing (duck typing) in TypeScript?

💡 Hint: Compatibility is based on shape, not name — if it has the right properties, it is the right type

TypeScript uses structural typing: two types are compatible if they share the same shape (property names and types), regardless of their declared names. This is unlike nominal typing systems (Java, C#) where the type name matters.

interface Point {
  x: number;
  y: number;
}

// An object literal with the right shape is assignable
const p: Point = { x: 1, y: 2 }; // ✅

// A class with the right shape is also assignable
class Vector {
  constructor(public x: number, public y: number) {}
}
const v: Point = new Vector(3, 4); // ✅ — Vector is structurally compatible

// Extra properties are fine when assigning to a less specific type
const extended = { x: 1, y: 2, label: 'origin' };
const p2: Point = extended; // ✅ — subset assignment (not a fresh literal)

// ❌ Fresh object literals trigger excess property checks
const p3: Point = { x: 1, y: 2, label: 'origin' }; // Error: 'label' not in Point

Practical implications:

  • Functions accepting a type will accept any superset — enables flexible APIs
  • You don't need implements to satisfy an interface — the shape is enough
  • Excess property checks only apply to fresh object literals
function printPoint(p: Point) { console.log(p.x, p.y); }

// All of these work — all have x and y:
printPoint({ x: 1, y: 2 });
printPoint(new Vector(1, 2));
printPoint({ x: 1, y: 2, z: 3 }); // extra props OK in this context
💡 Structural typing is why TypeScript feels flexible. You can define interfaces after the fact to describe shapes that already exist — great for working with third-party code.
Practice this question →
Q5Core

What are union types and intersection types? When do you use each?

💡 Hint: Union = A OR B (use narrowing to distinguish); intersection = A AND B (merged shape)

Union types (A | B) — a value can be one of several types. You need type narrowing to use type-specific operations.

type StringOrNumber = string | number;

function format(val: StringOrNumber): string {
  if (typeof val === 'string') {
    return val.toUpperCase(); // narrowed to string
  }
  return val.toFixed(2); // narrowed to number
}

// Discriminated union — literal type narrows the variant
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number };

function area(s: Shape) {
  switch (s.kind) {
    case 'circle': return Math.PI * s.radius ** 2;
    case 'square': return s.side ** 2;
  }
}

Intersection types (A & B) — a value must satisfy ALL types simultaneously. Useful for composing object shapes.

type Serializable = { serialize(): string };
type Loggable = { log(): void };

type SerializableLogger = Serializable & Loggable;
// Must have both serialize() AND log()

// Merging object types
type Admin = User & { adminLevel: number };

// Conflict: if A and B have same property with incompatible types
type Conflict = { id: string } & { id: number };
// id becomes: string & number = never — unusable
💡 Use union for "one of these options"; use intersection for "must satisfy all of these". Discriminated unions are the most powerful pattern for modeling state machines and API responses.
Practice this question →
Q6Core

What are type guards and how do you create custom ones?

💡 Hint: Narrowing the type in a branch — typeof, instanceof, in operator, or a user-defined predicate function

Type guards narrow a broad type to a more specific one within a conditional block.

Built-in type guards:

function process(val: string | number | null) {
  if (typeof val === 'string') { /* val is string */ }
  if (typeof val === 'number') { /* val is number */ }
  if (val !== null) { /* val is string | number */ }
}

// instanceof — for class instances
function handleEvent(e: MouseEvent | KeyboardEvent) {
  if (e instanceof KeyboardEvent) {
    console.log(e.key); // safe
  }
}

// in operator — property existence check
type Cat = { meow(): void };
type Dog = { bark(): void };
function speak(animal: Cat | Dog) {
  if ('meow' in animal) animal.meow();
  else animal.bark();
}

User-defined type guard — a function returning val is T:

interface User { id: number; name: string; }

// The return type "val is User" is the type predicate
function isUser(val: unknown): val is User {
  return (
    typeof val === 'object' &&
    val !== null &&
    'id' in val &&
    'name' in val &&
    typeof (val as User).id === 'number' &&
    typeof (val as User).name === 'string'
  );
}

// Usage — TypeScript now knows it's a User after the check
const data: unknown = fetchJSON();
if (isUser(data)) {
  console.log(data.name); // safe — TypeScript trusts your predicate
}
💡 Type predicates (val is T) are a contract you make with TypeScript. If your implementation is wrong, TypeScript won't catch it — always thoroughly validate all expected fields.
Practice this question →
Q7Core

What is the difference between type and interface in TypeScript?

💡 Hint: interface is extendable and mergeable; type is more powerful (unions, tuples, mapped types) but not mergeable

Both define object shapes, but they have different capabilities:

Interface — designed for object/class shapes, supports declaration merging:

interface User {
  id: number;
  name: string;
}

// Extending interfaces
interface Admin extends User {
  adminLevel: number;
}

// Declaration merging — useful for augmenting library types
interface Window {
  myCustomProp: string; // adds to existing Window interface
}

// Implementing in a class
class ConcreteUser implements User {
  constructor(public id: number, public name: string) {}
}

Type alias — more powerful, works with any type including primitives, unions, tuples, and mapped types:

// Primitive alias
type ID = string | number;

// Union — interfaces can't do this
type Status = 'active' | 'inactive' | 'pending';

// Tuple
type Pair = [string, number];

// Mapped type — only type aliases can do this directly
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// Conditional type
type NonNullable<T> = T extends null | undefined ? never : T;

Practical guidance:

  • Use interface for public API shapes that consumers may want to extend
  • Use type for unions, computed types, tuples, and function signatures
  • Be consistent within a codebase — consistency matters more than which one you pick
💡 The React team recommends interface for component props and type for everything else. Both work for most cases — pick a style and stick to it.
Practice this question →
Q8Advanced

What are literal types and how are they used for precise typing?

💡 Hint: A literal type narrows a string/number/boolean to a specific value — combines with union for exhaustive enumerations

Literal types narrow a broad primitive type to a specific value. "circle" is a subtype of string, 42 is a subtype of number.

// String literals
type Direction = 'north' | 'south' | 'east' | 'west';
function move(dir: Direction) { /* only 4 valid inputs */ }

move('north'); // ✅
move('up');    // ❌ Error

// Numeric literals
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

// Boolean literals — useful for discriminated unions
type Result<T> =
  | { success: true;  value: T }
  | { success: false; error: string };

function process(r: Result<number>) {
  if (r.success) console.log(r.value); // narrows via boolean literal
  else console.log(r.error);
}

const assertions — widen prevention:

// Without const — inferred as string
const dir = 'north'; // type: string (widened)

// With const — inferred as literal type
const dir2 = 'north' as const; // type: "north"

// Object with const — all properties become readonly literals
const config = { host: 'localhost', port: 3000 } as const;
// type: { readonly host: "localhost"; readonly port: 3000 }

// Array with const — tuple instead of array
const tuple = [1, 'a'] as const; // type: readonly [1, "a"]
💡 as const is the modern replacement for enums in many cases. It's a plain JavaScript value (no runtime overhead) while providing the exhaustive type checking of an enum.
Practice this question →

Other Typescript Interview Topics

Rendering Strategies →Core JS →React Fundamentals →Functions →Microfrontends →Generics →Async JS →Hooks →Objects →Monorepo →Arrays →'this' Keyword →Utility Types →Error Handling →Modern JS →Bundle Optimization →Performance →DOM & Events →State Management →Classes & OOP →Caching Strategies →Component Patterns →Advanced Types →Authentication →React Router →Forms →Advanced Patterns →Frontend Security →Concurrent React →Server Components →Testing →Ecosystem →Network Optimization →Core Web Vitals →Browser APIs →

Ready to practice Type System?

Get AI feedback on your answers, predict code output, and fix real bugs.

Start Free Practice →