Skip to content
>_ StartupJobList_

Interview questions · Worked answers

TypeScript Interview Questions: 10 Worked Answers

Explain what the checker can prove, where the value is still untrusted, and how a contract change reaches callers.

Intermediate TypeScript developers · TypeScript static contracts and their boundary with JavaScript runtime behavior. Compiler-version-specific options are identified where relevant.

Why use `unknown` at a boundary?

unknown says a value exists but its shape has not been established. Unlike any, it prevents property access until code narrows or validates the value. This makes uncertainty visible where it enters the program.

The checks are runtime JavaScript. TypeScript follows them and permits the final string return. The in test alone does not prove the property is a string, so the second check matters. A production parser needs an explicit policy for every field, unknown keys and nested values.

The in check narrowing an unknown object to a property-bearing shape relies on the TypeScript 4.9 change. In older projects, use a guard written for the installed compiler and verify it with that version.

TS · Worked example
function readId(value: unknown): string {
  if (typeof value !== 'object' || value === null || !('id' in value)) {
    throw new TypeError('expected an object with id');
  }
  const id = value.id;
  if (typeof id !== 'string') throw new TypeError('id must be a string');
  return id;
}

The guards establish that id is a string before the function returns it. If value is null, lacks id, or has a numeric id, the function throws instead of letting unchecked property access flow onward.

Illustrative, not executed.

Follow-up: Why not keep any for decoded JSON?

any disables checking as the value flows. unknown forces each use to establish the assumption first.

Read the source: TypeScript everyday types · narrowing · TypeScript 4.9: unlisted properties checked with in

↑ Back to questions

How do discriminated unions prevent an omitted state?

Use a shared literal field to distinguish each valid shape, then handle every case. An exhaustive never assignment asks the checker to flag a new variant until the function is updated.

If Load gains a cancelled member, the default assignment no longer type-checks. This catches omission in the declared program model; it does not prove the business copy or transition logic is right. Validate external input before constructing one of these internal states.

TS · Worked example
type Load<T> =
  | { kind: 'loading' }
  | { kind: 'ready'; value: T }
  | { kind: 'failed'; message: string };

function describe<T>(state: Load<T>): string {
  switch (state.kind) {
    case 'loading': return 'Loading';
    case 'ready': return 'Ready';
    case 'failed': return state.message;
    default: {
      const impossible: never = state;
      return impossible;
    }
  }
}

Adding cancelled to Load makes the never assignment report the unhandled variant at compile time. This proves the declared union is exhaustively handled, not that every transition or user-facing message is correct.

Illustrative, not executed.

Follow-up: Why not use optional value and message on one interface?

That shape permits combinations such as “loading with a value and error.” A union encodes which fields coexist in each state.

Read the source: TypeScript narrowing · object types

↑ Back to questions

What does structural typing mean?

Compatibility is generally based on the members a value provides, not on the name of the interface it declares. A function that needs only close() can accept a wider object with that method. This makes adapters easy to compose, while also allowing accidental compatibility when two concepts happen to share a shape.

The checker verifies a member contract, not the runtime behavior of close. TypeScript's excess-property checks on fresh object literals are a useful typo check, but they do not turn interfaces into nominal runtime tags. If domain identity matters, model it explicitly with a discriminant or branded construction boundary.

TS · Worked example
interface Closeable { close(): void }
function finish(resource: Closeable) { resource.close(); }

const socket = { close() {}, send(_data: string) {} };
finish(socket); // extra capability is fine

A value with a callable close() is assignable even if its class has no explicit Closable declaration. Passing an object literal with extra fields can trigger excess-property checking, but storing that same value in a variable is not a nominal identity check.

Illustrative, not executed.

Follow-up: Does implements make an object an instance at runtime?

No. It is checked at compile time and erased; use runtime identity/tagging only when the application needs it.

Read the source: Type compatibility · classes

↑ Back to questions

When does a generic preserve a useful relationship?

A generic is useful when one input's type determines another part of the contract. An identity function preserves the caller's specific type; a generic that does not relate inputs and outputs can add ceremony without safety.

With noUncheckedIndexedAccess, the indexed read correctly includes undefined. Without that option, an ordinary array index may be typed as present even when the array is empty. A generic constraint can express the capability a function needs, but should not promise that arbitrary T has properties the constraint did not state.

TS · Worked example
function first<T>(items: readonly T[]): T | undefined {
  return items[0];
}
const id = first([{ id: 'a' }]); // element shape is retained, or undefined

The generic keeps the element shape { id: string } in the return type, while undefined represents an empty array. The function does not claim every T has an id; callers that need that property must add a constraint.

Illustrative, not executed.

Follow-up: Should every reusable function be generic?

No. Use a concrete type when the behavior only makes sense for one domain type. Add a type parameter when it preserves a caller-visible relationship.

Read the source: TypeScript generics · TSConfig noUncheckedIndexedAccess

↑ Back to questions

Why can a dictionary lookup still be missing?

An index signature describes the type of values when a key exists; it does not make every possible key present. With noUncheckedIndexedAccess, a lookup includes undefined, which forces the caller to handle absence.

This reflects runtime reality. A default such as labels[key] ?? fallback is appropriate only if missing entries truly have that fallback meaning. For required configuration, failing early with a useful message is safer than quietly displaying a placeholder.

TS · Worked example
const labels: Record<string, string> = { primary: 'Save' };
const label = labels['secondary']; // string | undefined with noUncheckedIndexedAccess
if (label === undefined) throw new Error('missing label');

Looking up a key not present in labels yields undefined under noUncheckedIndexedAccess. Use the fallback only if unknown labels are an accepted case; required configuration should fail with the missing key named.

Illustrative, not executed.

Follow-up: Is Record<string, T> the right type for every map?

No. A finite key union can expose misspellings; Map<K,V> may better express mutation and key identity. Match the representation to the domain.

Read the source: TSConfig noUncheckedIndexedAccess · TypeScript object types

↑ Back to questions

What does narrowing prove across mutation?

Narrowing follows control flow the compiler can reason about. If a property can change through an alias or asynchronous callback, a check made earlier may no longer describe the runtime value later. Capture a validated primitive when that is the intended snapshot, or re-check at the moment of use.

The local destination captures the string value. If the code instead rereads a mutable object later, another part of the program could change that property before the callback. Types do not provide locking or eliminate time-of-check/time-of-use races. For shared mutable state, use a state ownership or synchronization design.

TS · Worked example
function send(message: { destination?: string }) {
  const destination = message.destination;
  if (destination === undefined) return;
  queueMicrotask(() => deliver(destination));
}

The local destination stores the checked string before the callback runs, so a later mutation of message.destination cannot change that captured value. Re-reading the mutable property later would require a fresh check and synchronization policy.

Illustrative, not executed.

Follow-up: Can I use a non-null assertion to fix a narrowing error?

It suppresses that check; use it only when an invariant is established elsewhere and documented/tested. It is not a runtime check.

Read the source: TypeScript narrowing · TypeScript everyday types and assertions

↑ Back to questions

What happens to types at runtime?

Type annotations and interfaces are erased from emitted JavaScript; the runtime sees only the remaining values and expressions. If code needs to branch on a type at runtime, represent that fact as data, such as a discriminant field, instead of expecting an interface declaration to exist.

An interface can help the checker at compile time but creates no JavaScript constructor or metadata to inspect later. This is a build-output boundary: the source and emitted program contain different information. Classes and enums may emit runtime values, so check the selected compiler output before relying on one.

TS · Worked example
interface Account { id: string }
const account: Account = { id: "acct-7" };
console.log(typeof account); // "object"
// emitted JavaScript: const account = { id: "acct-7" };

The annotation and interface disappear, while the object literal remains. typeof account reports the JavaScript category "object"; it cannot tell whether the value satisfied the TypeScript Account interface.

Illustrative, not executed.

Follow-up: Do enums and classes exist at runtime?

Some TypeScript constructs emit JavaScript values, while interfaces and most type-only declarations do not. Inspect the actual output/toolchain before making an architectural claim; a runtime tag should be deliberately represented as a value.

Read the source: TypeScript Handbook · TypeScript everyday types

↑ Back to questions

What does `strict` buy, and what does it cost?

The strict compiler option enables a family of stronger checks, including strict null handling. It makes missing values and implicit assumptions more visible, but can require annotations and cleanup when enabled in an older codebase. It does not certify correctness or security.

For a migration, enable strictness at a bounded project/module boundary, address errors rather than blanket-casting them away, and make the same check run in CI. Some teams also enable noUncheckedIndexedAccess because array and map lookups can be absent even when strict mode is on. Confirm each option in the project's actual TS version and config.

Follow-up: Can a project safely use different strictness by file?

TypeScript compiler settings are primarily project-wide, though staged migration patterns and checked JS options can scope adoption. Choose a reproducible configuration rather than relying on editor-only behavior.

Read the source: TSConfig strict · TypeScript migration from JavaScript

↑ Back to questions

What is the difference between an annotation and `satisfies`?

An annotation states the variable's type, which can widen its visible type to that annotation. satisfies checks compatibility while retaining the expression's more specific inferred type. It is useful for configuration tables whose values should be checked against a contract while preserving literal information.

This does not validate a JSON file at runtime; the object literal is checked during compilation. The operator was added in TypeScript 4.9, so a project must use a supporting compiler. Do not choose it solely for style when a direct annotation is clearer.

TS · Worked example
type Route = { method: 'GET' | 'POST'; auth: boolean };
const routes = {
  health: { method: 'GET', auth: false },
  update: { method: 'POST', auth: true },
} satisfies Record<string, Route>;

The table must give every route a permitted method and an auth boolean. routes.health.method retains its inferred GET literal type. With an explicit Record<string, Route> annotation, a named lookup is viewed through the broader record contract instead. Neither form validates configuration loaded at runtime or freezes the object.

Illustrative, not executed.

Follow-up: Does satisfies make the object immutable?

No. It checks a type relationship; it does not freeze the runtime object. Use readonly types or runtime freezing only when mutation is part of the actual concern.

Read the source: TypeScript 4.9 release notes · object types

↑ Back to questions

What belongs in a public signature?

A public signature should express the behavior callers can rely on: accepted values, optionality, return state and mutation. Prefer readonly inputs when the function should not mutate caller-owned arrays or objects. Avoid exposing internal implementation types that force every caller to change when internals are reorganized.

This signature prevents this function from mutating the array through that reference, but it does not make the original array immutable everywhere. If values can be negative or non-finite, the domain contract needs additional validation; the type number includes them.

TS · Worked example
function total(prices: readonly number[]): number {
  return prices.reduce((sum, price) => sum + price, 0);
}

readonly number[] prevents this function from writing through its parameter, but callers can still mutate an array through another alias. The signature also accepts negative and non-finite numbers, so enforce those domain rules separately if they are invalid.

Illustrative, not executed.

Follow-up: Should the type encode every business rule?

Encode stable distinctions that callers need to handle. Keep volatile policy in explicit runtime validation and domain logic; a type that claims more than constructors enforce is misleading.

Read the source: TypeScript object types · TypeScript everyday types

↑ Back to questions

Salary context

Advertised salaries for these skills

These are Hacker News job posts that mention these technologies, not employee earnings or a measure of what a skill adds to pay. The amounts summarize annualized advertised minimums in approximate USD. Groups can overlap and differ in roles, locations and dates.

TypeScript posts

451 posts · 451 usable advertised salary amounts

2024-07-01 to 2026-10-14

$147,000

Median advertised minimum, approximate USD/year

Middle 50%: $105,763–$170,000

Explore TypeScript salaries → Browse matching job posts →
Currency and calculation basis

Approximate USD using ECB daily reference rates as of 2026-10-09. Original currencies and offer ranges remain on the source listings. These figures cover posts tagged TypeScript, rather than every job in the broader technology family.

↑ Back to questions

Keep exploring