Skip to content
SDETRole
SDETRole

TypeScript interview question

TypeScript types disappear at runtime. How do you make sure API responses actually match your types?

advanced 5+ yrs #runtime-validation#zod#json-schema#contract-testing#type-safety

Short answer

Types are erased on compilation, so res.json() as User is a cast, not a check. Validate at the boundary with a schema library such as Zod or JSON Schema with Ajv, infer the static type from the schema so there is one source of truth, and fail the test with a readable error when the contract drifts.

Answer

interface User { ... } and res.json() as User only affect the compiler. After compilation the types are gone, and as performs no check, so an API that returns "total": "19.99" instead of a number passes straight through your typed client and fails somewhere far from the cause, or worse, never fails. To make types true, validate data where it enters your code and derive the static types from the same schema that does the validation.

The problem

type Order = { id: string; total: number; status: 'paid' | 'pending' };

const order = (await res.json()) as Order; // a promise to the compiler, not a check
expect(order.total + 1).toBe(20.99);       // "19.991" if total is a string

Option 1: Zod (schema first, type inferred)

import { z } from 'zod';
import { test, expect } from '@playwright/test';

const OrderSchema = z.object({
  id: z.string(),
  total: z.number().nonnegative(),
  status: z.enum(['paid', 'pending']),
  items: z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })),
});
type Order = z.infer<typeof OrderSchema>; // single source of truth

test('GET /orders/:id matches the contract', async ({ request }) => {
  const res = await request.get('/api/orders/7');
  expect(res.status()).toBe(200);

  const parsed = OrderSchema.safeParse(await res.json());
  expect(parsed.success, parsed.success ? '' : JSON.stringify(parsed.error.issues, null, 2)).toBe(true);

  const order: Order = parsed.data!;
  expect(order.status).toBe('paid');
});

safeParse returns a result instead of throwing, so the failure message lists exactly which path was wrong. Inside helpers, OrderSchema.parse(json) (which throws) is fine. z.object strips unknown keys by default. If extra fields should fail, use z.strictObject(...) in Zod 4 (.strict() in Zod 3); to keep them, use z.looseObject(...) in Zod 4 (.passthrough() in Zod 3).

Option 2: JSON Schema with Ajv

If the team already has an OpenAPI spec, its JSON Schemas are the contract. Ajv compiles them into fast validators:

import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

const validateOrder = ajv.compile(orderSchemaFromOpenApi);

const body: unknown = await res.json();
if (!validateOrder(body)) {
  throw new Error(ajv.errorsText(validateOrder.errors));
}

Types can be generated from the same spec (openapi-typescript) or inferred with json-schema-to-ts, so the schema remains the single source.

Option 3: hand-written type guards

For a couple of small shapes, a (v: unknown): v is Order guard works, but it duplicates the type and drifts. It does not scale beyond a few endpoints.

Design decisions for a test framework

  • Validate at the boundary: inside the API client or a fixture, so every test benefits and test bodies stay clean.
  • Strict or lenient? For contract tests owned by the API team, failing on unexpected fields catches undocumented changes. For consumer-side UI tests, lenient parsing avoids breaking when fields are added.
  • Schema validation is not a business assertion. It proves the shape; you still assert the values that matter for the scenario.
  • Contract testing across teams: consumer-driven contracts (Pact) or OpenAPI diffing in CI catch breaking changes before deployment, which a schema check in an E2E test catches only after.

What interviewers look for

  • Precise understanding of type erasure and why as is unsafe.
  • A concrete validator with types inferred from the schema.
  • Where validation lives in the framework and readable failure output.
  • Judgement about strictness and how this relates to contract testing.

A strong answer covers

  • Explains type erasure: casts and generics are not runtime checks
  • Uses a schema validator such as Zod, Ajv or Valibot
  • Infers the TypeScript type from the schema for a single source of truth
  • Uses unknown and type guards at the boundary
  • Connects to contract testing, OpenAPI or strictness choices
Practise answering under time pressure

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →