TypeScript – Interview Questions

September 5, 20268 min readUpdated 9/5/2026

The questions a TypeScript role actually asks, answered against the twenty lessons before this one. Short answers with the reasoning attached, because an interviewer is usually listening for the second part.

What is TypeScript, in one sentence?

A type checker for JavaScript whose annotations are erased at build time, so nothing it does exists at runtime.

The follow-up is almost always some version of so what does it not protect you from? Anything crossing a boundary — an API response, localStorage, a URL parameter. You can declare what you expect and the compiler will believe you. Saying that unprompted is worth more than the definition.

any versus unknown?

Both accept any value. any then lets you do anything with it and stops checking; unknown lets you do nothing until you have proved what it is.

function report(err: unknown) {
  if (err instanceof Error) {
    console.log(err.message);   // only legal after the check
  }
}

The point to make is that any spreads: assign it to something else and that becomes unchecked too, with no error anywhere to tell you how far it went. unknown is the default choice, and catch gives you one automatically under strict.

interface or type?

They overlap almost entirely. Three real differences:

  1. type can name anything — a union, a primitive, a tuple, a conditional. interface describes object shapes only.
  2. Interfaces merge across declarations, which is the only way to extend a type from a library you do not own.
  3. interface extends reports a conflicting property at the declaration; an intersection silently produces never and fails at every use site instead.

Practical answer: interface for a named object shape, type for everything a shape cannot express. Anyone claiming one is categorically better has not hit the second or third case.

Why do people avoid enum?

Because it is the only construct in the language that emits runtime JavaScript. Everything that follows is a consequence: numeric enums generate a reverse mapping so Object.keys returns twice what you wrote; const enum cannot cross a module boundary under isolatedModules, which rules it out for most bundlers; and erasableSyntaxOnly rejects enum outright.

The replacement is a union of string literals, with an as const object when you also need the values at runtime:

export type OrderStatus =
  | 'PENDING_PAYMENT'
  | 'PAID'
  | 'PREPARING'
  | 'COMPLETED'
  | 'CANCELLED';

A good closing line: enums are not wrong, they are just no longer the obvious choice, because the alternative does everything they do and compiles everywhere.

What does structural typing mean in practice?

Compatibility is decided by shape, not by name or declared relationship:

function label(thing: { name: string }) {
  return thing.name;
}

label(product);   // fine
label(topping);   // fine — no shared base type, no implements

Two consequences worth volunteering. You can describe data you do not own — the server's JSON never has to implement anything. And small parameter types are better: a function that reads a name should ask for { name: string }, not Product.

The exception is private class members, which are compared nominally — two identically-shaped classes with private fields are not interchangeable.

How does narrowing work?

TypeScript follows your control flow and applies the checks you have written. typeof for primitives, instanceof for classes, in for properties, truthiness, and equality against a literal.

The two you write yourself are the type predicate:

function isProduct(value: unknown): value is Product { /* … */ }

and the discriminated union, which is the one to lead with:

export type Outcome = { ok: true } | { ok: false; failure: ApiFailure };

Checking result.ok settles the whole shape, so failure is unreachable on success and guaranteed on failure. Compare with one interface carrying failure?: ApiFailure, which permits a success carrying a failure.

A strong follow-up to offer unasked: narrowing is lost inside callbacks, because the compiler cannot know when they run. The fix is a const copy after the check.

How would you make sure every case is handled?

Assign the narrowed-out value to never:

    default: {
      const unreachable: never = status;
      return unreachable;
    }

While every case is handled, status in the default is never and it compiles. Add a member to the union and it does not — pointing at the function that was not updated.

The other half of the answer is Record<OrderStatus, string> for a lookup: same guarantee, one annotation, no default branch.

What do generics actually buy you?

They preserve a relationship between types that would otherwise be lost. The canonical case is an HTTP wrapper:

async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {

Without T the return type is any, which gives up checking, or unknown, which forces an assertion at every call site. With it, the caller states its expectation once.

Two things to add. If a type parameter appears only once it is doing nothing — the whole purpose is to tie two positions together. And it does not validate anything: api.get<User> is a claim about the response, not a check on it.

Which utility types do you use?

Name the ones you have actually used and say what for. Partial for patch payloads, Pick and Omit for derived shapes, Record for lookups, ReturnType with typeof for deriving a type from a value.

The one to lead with is Omit doing real work:

  get: <T>(path: string, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
    request<T>(path, { ...opts, method: 'GET' }),

The wrapper has already decided the method and body, so it removes them from what a caller may pass. Hand-writing that type would work until RequestOptions gained a field.

Then the underlying point: derive rather than copy, because a copy is the thing that falls out of step.

When is `as` acceptable?

When you know something the compiler cannot: the shape of parsed JSON, the element a querySelector will find, a payload guaranteed by a check the type system cannot follow. Confine it to the boundary module and leave a comment.

When it is not acceptable: to silence an inconvenient error in business logic. There, narrowing would have proved the same thing honestly.

Have satisfies ready, because it is what most as uses actually wanted:

const STATUS_LABEL = {
  PENDING_PAYMENT: 'Awaiting payment',
  PAID: 'Paid',
} satisfies Record<OrderStatus, string>;

An annotation checks the keys but widens the values to string. No annotation keeps the values precise but checks nothing. satisfies does both.

What is erased at build time, and what is not?

A good question to be crisp on, because it separates people who have read the manual from people who have shipped.

Erased: type annotations, interface, type, implements, abstract, generics, private and readonly, as, import type.

Emitted: enum, parameter properties (constructor(private x: T)), decorators, and namespace with runtime members.

That second list is exactly what erasableSyntaxOnly bans — and knowing the flag exists, and why, is a good signal.

What is in strict?

strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict.

strictNullChecks is most of the value: without it every type silently includes null. On a migration, enable them one at a time and do noImplicitAny first.

Worth mentioning two that are not in strict and probably should be on new code: noUncheckedIndexedAccess, which stops arr[0] pretending to exist, and exactOptionalPropertyTypes.

How do you add TypeScript to an existing JavaScript project?

Gradually, and without renaming anything on day one. allowJs plus checkJs typechecks the existing .js files in place, inferring from the code and any JSDoc, and usually finds real bugs before a single annotation is written.

Then: get green with strict off, rename to .ts a few leaf files at a time, and turn on the strict flags individually. What does not work is renaming everything and enabling strict in one commit — that produces four thousand errors and a branch nobody merges.

What is the difference between an annotation, an assertion and satisfies?

A three-row answer, and being able to give it cleanly says a lot:

FormChecks the value?Changes the inferred type?
const x: T = …yesyes — widens to T
const x = … as Tnoyes — forces T
const x = … satisfies Tyesno

The follow-up worth pre-empting is why the middle row matters: an assertion is the only one of the three that can produce a program which compiles and then crashes.

How do you type an API call?

One generic wrapper, one place where the claim is made:

const me = await api.get<User>('/api/auth/me', { auth: true });

Then say what that does not do — it does not validate the response — and name the three options: trust the contract, generate the types from a schema, or parse with a runtime validator. Which one is right depends on whether you own the API and how bad a mismatch would be.

The part interviewers listen for is whether you know the difference between a type and a guarantee.

Anything you dislike about it?

Asked more often than people expect, and "nothing" is a weak answer. Reasonable ones:

The boundary problem — the type system's guarantees stop exactly where your data comes from, and it is easy to forget that a declared type is a claim.

Type-level programming has no debugger. A wrong conditional type is diagnosed by staring, and deeply recursive ones make the editor slow.

Two decorator systems, incompatible, one of them still called experimental years after the frameworks standardised on it.

Each of those is a real thing you would only know from use, which is the point of the question.

A closing note on how to answer these

Nearly every question above has a one-line answer and a second sentence that shows you have used the thing. The second sentence is what is being listened for — "unknown is the safe any" is a definition, and "so I use it for caught errors and for request bodies I only forward" is experience.

That is the track

Twenty-one lessons, from what erasure means to how a React context avoids an undefined. If one idea is worth carrying out of all of it, it is the one this last lesson keeps returning to: derive, do not copy. Every type written twice is a type that will disagree with itself, and most of what this language gives you is a way to say something once.

Back to the beginning for the full index.