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 initializationconst x = 42 infers number
  • Function return type — inferred from the return expression
  • Default parametersfunction 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 StrategiesCore JSReact FundamentalsFunctionsMicrofrontendsGenericsAsync JSHooksObjectsMonorepoArrays'this' KeywordUtility TypesError HandlingModern JSBundle OptimizationPerformanceDOM & EventsState ManagementClasses & OOPCaching StrategiesComponent PatternsAdvanced TypesAuthenticationReact RouterFormsAdvanced PatternsFrontend SecurityConcurrent ReactServer ComponentsTestingEcosystemNetwork OptimizationCore Web VitalsBrowser APIs

Ready to practice Type System?

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

Start Free Practice →