TL;DR: Senior TypeScript interviews in 2026 test whether a candidate can model a domain in the type system and explain where types stop: generics, conditional and mapped types, narrowing, variance, and the line between compile time and runtime. Candidates should also know that TypeScript 7.0, the Go-based native compiler, is the current release, that TypeScript 6.0 changed the tsconfig defaults, and which syntax Node.js can run without a build step.
On July 8, 2026, the TypeScript team shipped TypeScript 7.0, a port of the compiler to Go that it says usually builds 8 to 12 times faster than TypeScript 6.
The type system did not change with it, but the defaults did: strict is now on unless you turn it off.
The 30 questions below cover the language, the compiler settings that decide how code resolves and runs, and the 2024 to 2026 changes a working TypeScript engineer should already know.
- 1TypeScript 7.0 ships no compiler API. Tools such as typescript-eslint, and Vue, Astro or Svelte editor tooling, still run on TypeScript 6.0 until 7.1.
- 2Since TypeScript 6.0,
typesdefaults to an empty array, so globals from@types/nodevanish until you list them. - 3Method-syntax parameters are still checked bivariantly even under
strict, which lets some unsound assignments compile. - 4Node.js strips types but refuses to run .ts files inside node_modules, so packages must still ship JavaScript.
Type System Basics
1. TypeScript is structurally typed. What does that mean, and where does it surprise developers?
Compatibility depends on shape, not on declared names: a value fits a type if it has every required property with a compatible type.
Two interfaces with the same members are interchangeable, and a class instance can satisfy an interface it never mentions.
The surprise is the excess property check. It applies only to a fresh object literal assigned straight to a typed target. The same object passed through a variable is accepted:
interface Point { x: number; y: number }
const p3 = { x: 1, y: 2, z: 3 };
const a: Point = p3; // OK: p3 has every property Point needs
// @ts-expect-error Object literal may only specify known properties
const b: Point = { x: 1, y: 2, z: 3 };
A strong answer adds the consequence: structural typing cannot tell a UserId string from an OrderId string. When that matters, you need a brand (question 16).
2. When do you use any, unknown and never?
unknown is the safe top type: anything can be assigned to it, but you must narrow it before use. any switches checking off in both directions and spreads silently through every expression it touches. never is the empty type: no value has it, so it marks code that cannot be reached and functions that never return.
function parsePort(raw: unknown): number {
if (typeof raw === "number") return raw;
if (typeof raw === "string" && /^\d+$/.test(raw)) return Number(raw);
throw new Error("invalid port");
}
function fail(message: string): never {
throw new Error(message);
}
Data from JSON.parse, fetch or a message queue should enter the program as unknown. Under strict, useUnknownInCatchVariables already types catch variables that way.
3. Interface or type alias: does the choice still matter?
For plain object shapes, rarely. The real differences are few. Interfaces support declaration merging, which is how you augment a library's types. Only type aliases can name unions, tuples, mapped and conditional types.
And interface extends reports conflicting members as an error, while an intersection quietly produces never for them.
There is also a compiler cost. The TypeScript team's performance wiki recommends interfaces over intersections for composed object types.
Type relationships between interfaces are cached, intersections as a whole are not, and interfaces report property conflicts that intersections hide.
A team rule such as "interfaces for object shapes, aliases for everything else" is a reasonable answer, as long as the candidate can say why.
4. What does strict turn on, and what does it leave off?
Per the tsconfig reference, strict enables the whole strict family: alwaysStrict, strictNullChecks, strictBindCallApply, strictBuiltinIteratorReturn, strictFunctionTypes, strictPropertyInitialization, noImplicitAny, noImplicitThis and useUnknownInCatchVariables.
The docs warn that future versions may add checks under the flag, so a compiler upgrade can surface new errors.
Two useful flags sit outside it. noUncheckedIndexedAccess adds undefined to every index-signature and array read. exactOptionalPropertyTypes separates "property missing" from "property set to undefined".
Since TypeScript 6.0, strict is true by default, so an older project that never set it will see a wave of new errors on upgrade (question 26).
5. How does narrowing work, and how do you make a switch exhaustive?
The compiler follows control flow and refines a variable's type after checks such as typeof, instanceof, in, equality and truthiness, as the narrowing handbook describes.
With a discriminated union, checking the shared literal field narrows to one member. Passing the leftover value to a function that takes never turns a forgotten case into a compile error:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number };
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.radius ** 2;
case "rect":
return s.width * s.height;
default:
return assertNever(s); // compile error if a new kind is added
}
}
Follow up by asking what breaks narrowing. A narrowed property read inside a callback, or a mutable variable reassigned in a closure, may lose its refinement. The compiler cannot prove the value is unchanged when the callback runs.
6. Type predicate or assertion function: what is the difference, and what did TypeScript 5.5 add?
A type predicate (v is User) returns a boolean and narrows in the branch where it is true. An assertion function (asserts v is User) returns nothing and throws on failure, so the narrowing applies to all code after the call.
const ids = [1, null, 2, undefined].filter((x) => x != null);
// ^? number[] since TypeScript 5.5
interface User { id: string; email: string }
function isUser(v: unknown): v is User {
return typeof v === "object" && v !== null &&
typeof (v as User).id === "string" && typeof (v as User).email === "string";
}
function assertIsUser(v: unknown): asserts v is User {
if (!isUser(v)) throw new TypeError("not a User");
}
declare const body: unknown;
assertIsUser(body);
body.email.toLowerCase(); // body is User from here on
The TypeScript 5.5 release (June 20, 2024) added inferred type predicates, which is why the filter call above returns number[] without a hand-written guard. The senior point: the compiler trusts a predicate completely.
A predicate that checks only id and claims User is a lie the rest of the program inherits.
Generics and Variance
7. Write a typed pick function. Why does keyof need a constraint?
The key parameter must be constrained to the keys of the object, or the compiler cannot check that obj[k] exists:
function pick<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
for (const k of keys) out[k] = obj[k];
return out;
}
const user = { id: "u1", email: "a@b.co", age: 31 };
const contact = pick(user, ["id", "email"]); // { id: string; email: string }
// @ts-expect-error "name" is not a key of user
pick(user, ["name"]);
Good candidates point out that K is inferred as the union "id" | "email" from the array literal. They also note that the as Pick<T, K> cast on the empty object is the one place this function relies on trust.
A weak answer types the keys as string[] and returns any.
8. What do const type parameters and NoInfer solve?
Both control inference. A const type parameter (TypeScript 5.0) infers literal and readonly tuple types as if the caller had written as const.
NoInfer<T> (TypeScript 5.4) stops one argument from contributing to inference, so it is checked against what the other arguments decided.
declare function routes<const T extends readonly string[]>(paths: T): T;
const r = routes(["/home", "/settings"]);
// ^? readonly ["/home", "/settings"]
function createMachine<S extends string>(states: S[], initial: NoInfer<S>) {
return { states, current: initial };
}
createMachine(["idle", "running"], "idle"); // OK
// @ts-expect-error "stopped" is not one of the listed states
createMachine(["idle", "running"], "stopped");
Without NoInfer, "stopped" would widen S to include it and the typo would compile.
9. Explain variance with function parameters. Where is TypeScript unsound on purpose?
A function that accepts an Animal can stand in for one that accepts a Dog, but not the other way around: parameters are contravariant.
Under strictFunctionTypes the compiler enforces this, but the tsconfig docs say it applies only to function-syntax types, not methods, because too many existing class hierarchies (the DOM included) relied on method bivariance.
interface Animal { name: string }
interface Dog extends Animal { bark(): void }
type Handler<T> = (value: T) => void;
let handleAnimal: Handler<Animal> = (a) => console.log(a.name);
let handleDog: Handler<Dog> = (d) => d.bark();
handleDog = handleAnimal; // OK: parameters are contravariant
// @ts-expect-error a Dog handler cannot take any Animal
handleAnimal = handleDog;
interface Sink<T> { write(value: T): void } // method syntax: checked bivariantly
const dogSink: Sink<Dog> = { write: (d) => d.bark() };
const animalSink: Sink<Animal> = dogSink; // compiles, but unsound
Writing callbacks as properties (write: (value: T) => void) restores the strict check.
Senior candidates may also mention the in and out variance annotations from TypeScript 4.7, which declare a type parameter's variance instead of letting the compiler measure it.
10. satisfies, a type annotation or as: when is each right?
An annotation sets the variable's type and forgets anything more specific. satisfies (TypeScript 4.9) checks the value against a type but keeps the inferred type. as overrides the compiler and checks almost nothing.
type Env = "dev" | "staging" | "prod";
const apiUrl = {
dev: "http://localhost:3000",
staging: "https://staging.example.com",
prod: new URL("https://api.example.com"),
} satisfies Record<Env, string | URL>;
apiUrl.dev.startsWith("http"); // still string, not string | URL
apiUrl.prod.hostname; // still URL
const widened: Record<Env, string | URL> = apiUrl;
// @ts-expect-error startsWith does not exist on string | URL
widened.dev.startsWith("http");
Use satisfies for config objects and lookup tables, where you want both a completeness check (a missing prod key is an error) and precise property types. Use an annotation at API boundaries, where callers should see the declared type.
Treat every as in a code review as a claim that needs a reason.
Type-Level Programming
11. What is a distributive conditional type, and how do you turn distribution off?
When a conditional type checks a naked type parameter, it is applied to each member of a union separately and the results are joined, as the conditional types handbook explains. Wrapping both sides in a one-element tuple stops it:
type ToArray<T> = T extends unknown ? T[] : never;
type A = ToArray<string | number>; // string[] | number[]
type ToArrayOnce<T> = [T] extends [unknown] ? T[] : never;
type B = ToArrayOnce<string | number>; // (string | number)[]
type IsNever<T> = [T] extends [never] ? true : false;
type C = IsNever<never>; // true
The IsNever case is the classic follow-up. A naked T extends never returns never for never input, because distributing over an empty union produces an empty union. Built-ins such as Exclude and Extract depend on distribution.
12. How does infer work? Write a type that unwraps nested promises.
infer declares a type variable inside the extends clause of a conditional type, and the compiler fills it by matching the pattern. Recursion handles nesting:
type Unwrap<T> = T extends PromiseLike<infer U> ? Unwrap<U> : T;
type FirstArg<F> = F extends (first: infer A, ...rest: never[]) => unknown ? A : never;
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type X = Unwrap<Promise<Promise<number>>>; // number
type Y = FirstArg<(id: string, retries: number) => void>; // string
type Z = ElementOf<readonly ["a", "b"]>; // "a" | "b"
A candidate who knows the standard library will say that Awaited, ReturnType and Parameters already exist and are built the same way, and will reach for them first. Writing your own is for shapes the library does not cover.
13. Use a mapped type with key remapping to generate getters and drop methods.
The as clause in a mapped type renames each key, and mapping a key to never removes it. Per the mapped types handbook, this combines with template literal types and the intrinsic string helpers:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type DataOnly<T> = {
[K in keyof T as T[K] extends (...args: never[]) => unknown ? never : K]: T[K];
};
interface Account { id: string; balance: number; close(): void }
type AccountGetters = Getters<DataOnly<Account>>;
// { getId: () => string; getBalance: () => number }
The string & K matters because keyof T can include number and symbol keys, which Capitalize does not accept. Ask the candidate why: it shows whether they read the error or copied the pattern.
14. Use template literal types to type route parameters.
Template literal types can both build and take apart string types. A recursive pattern match pulls every :param out of a route:
type RouteParams<P extends string> =
P extends `${string}:${infer Name}/${infer Rest}`
? Name | RouteParams<`/${Rest}`>
: P extends `${string}:${infer Name}`
? Name
: never;
type P = RouteParams<"/orgs/:orgId/users/:userId">; // "orgId" | "userId"
function buildUrl<Path extends string>(
path: Path,
params: Record<RouteParams<Path>, string>,
): string {
return path.replace(/:(\w+)/g, (_, k: string) => params[k as RouteParams<Path>]);
}
buildUrl("/orgs/:orgId/users/:userId", { orgId: "acme", userId: "42" });
// @ts-expect-error userId is missing
buildUrl("/orgs/:orgId/users/:userId", { orgId: "acme" });
This is the technique typed routers use. A strong follow-up answer names the limits: the type only works when the path is a literal, and deep recursion on long strings costs compile time.
The template literal handbook covers the building blocks.
15. Write DeepReadonly. Which cases does the naive version get wrong?
The naive version is a recursive mapped type over T extends object.
It mishandles functions (they get mapped into objects that lose their call signature) and leaves Map and Set mutable, because their methods are not properties you can mark readonly. Handle each case before the generic object branch:
type DeepReadonly<T> =
T extends (...args: never[]) => unknown ? T
: T extends Map<infer K, infer V> ? ReadonlyMap<K, DeepReadonly<V>>
: T extends Set<infer U> ? ReadonlySet<DeepReadonly<U>>
: T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Config { db: { hosts: string[]; port: number }; flags: Set<string> }
declare const cfg: DeepReadonly<Config>;
// @ts-expect-error readonly all the way down
cfg.db.hosts.push("x");
// @ts-expect-error ReadonlySet has no add method
cfg.flags.add("beta");
Arrays work without a special case, because a homomorphic mapped type over an array or tuple produces a readonly array or tuple.
The candidate should also say that this is compile-time only: Object.freeze is still needed if other code may mutate the object at runtime.
16. How do you stop a UserId being passed where an OrderId is expected?
Brand the types. Intersect the base type with a property that exists only in the type system, and create branded values only through a validating function:
declare const brand: unique symbol;
type Brand<T, Name extends string> = T & { readonly [brand]: Name };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function toUserId(raw: string): UserId {
if (!/^usr_[a-z0-9]+$/.test(raw)) throw new Error("bad user id");
return raw as UserId;
}
declare function loadOrder(id: OrderId): void;
const uid = toUserId("usr_42");
// @ts-expect-error a UserId is not an OrderId
loadOrder(uid);
The unique symbol key cannot collide with a real property, and the value is still a plain string at runtime. The only as sits inside the constructor function, which is where a reviewer should look for it.
Runtime, Modules, Config
17. Types are erased. How do you make sure an API request body matches its type?
Validate at the boundary with a schema, and derive the static type from the schema so the two cannot drift. A cast such as req.body as CreateUser only silences the compiler. With Zod 4:
import * as z from "zod";
const CreateUser = z.object({
email: z.email(),
age: z.number().int().min(18),
role: z.enum(["admin", "member"]).default("member"),
});
export type CreateUser = z.infer<typeof CreateUser>;
export function parseCreateUser(body: unknown): CreateUser {
const result = CreateUser.safeParse(body);
if (!result.success) throw new Error(z.prettifyError(result.error));
return result.data; // checked at runtime, typed at compile time
}
The same rule applies to environment variables, third-party API responses and queue messages. The Node.js interview guide covers loading and validating configuration at startup.
Here the question is whether the candidate sees every unknown input as a place where the type system has no say.
18. Enum, union of string literals or a const object?
For new code, a union of literals, or a const object plus a derived union when you also want named values at runtime:
export const Status = {
Active: "active",
Suspended: "suspended",
} as const;
export type Status = (typeof Status)[keyof typeof Status]; // "active" | "suspended"
function label(s: Status): string {
return s === Status.Active ? "On" : "Off";
}
label("active"); // plain strings from JSON still type-check
label(Status.Suspended);
Enums are one of the few TypeScript features that emit runtime code.
Per the erasableSyntaxOnly docs, they are rejected by that flag and by Node.js type stripping. const enum adds a second problem: single-file transpilers cannot inline values from another file, which is one of the cases isolatedModules exists to flag.
A string enum also rejects a plain "active" string, which makes data from JSON awkward to pass in.
19. A dependency ships no types. How do you type it, and how do you add a global?
Write an ambient module declaration in a .d.ts file inside the project.
The detail candidates miss: the file must not contain a top-level import or export, or declare module becomes an augmentation of an existing module instead of a new declaration.
// types/legacy-slugify.d.ts (no top-level import or export)
declare module "legacy-slugify" {
export interface SlugOptions { lower?: boolean; separator?: string }
export default function slugify(input: string, options?: SlugOptions): string;
}
// types/globals.d.ts (a module, so it uses declare global)
export {};
declare global {
var __APP_VERSION__: string;
}
A senior engineer would also check DefinitelyTyped for an @types package first and contribute the declarations upstream if the package is maintained.
And since TypeScript 6.0, any global @types package has to be listed in types (question 26). The declaration files handbook is the reference for library authors.
20. How do you choose module and moduleResolution?
Pick the setting that matches what will load the code. The TypeScript team's guide to choosing compiler options gives the cases. For a bundled app, "module": "esnext" with "moduleResolution": "bundler" and noEmit.
For code compiled by tsc and run in Node.js, "module": "nodenext". For a library, node18 or nodenext with the strictest settings.

The library case is the one to probe.
The guide calls bundler resolution "infectious": it accepts export * from "./utils" without an extension, which bundlers handle but Node.js rejects with ERR_MODULE_NOT_FOUND once the package is published.
Code checked under nodenext almost always works in bundlers too. Since TypeScript 6.0, moduleResolution: node (also called node10) is deprecated, and 7.0 no longer supports it at all.
21. What do isolatedModules and verbatimModuleSyntax protect against?
Both make the code safe for tools that compile one file at a time, such as esbuild, SWC or Node.js type stripping. isolatedModules flags code a single-file transpiler cannot handle, such as a re-export of a type without the type keyword. verbatimModuleSyntax goes further: imports without the type modifier are kept in the output, and type-only imports are dropped, so what you write is what runs.
import { Status, type Status as StatusType } from "./status.ts";
import type { CreateUser } from "./schemas.ts";
export function describe(u: CreateUser, s: StatusType): string {
return `${u.email}: ${s === Status.Active ? "active" : "inactive"}`;
}
One trap when turning it on: in a package with no "type": "module", .ts files are treated as CommonJS, and verbatimModuleSyntax then rejects every import and export statement.
The fix is the package type or .mts files, not turning the flag off.
22. Type checking takes four minutes in CI. How do you find out why?
Measure before changing code. The performance wiki lays out the tools:
tsc --extendedDiagnosticssplits time into parse, bind, check and emit, and counts files, types and memory. A file count far above expectations points toincludeortypesproblems.--explainFilesand--traceResolutionshow why each file is in the program and how imports resolved.--generateTracewrites a trace that shows which files and types the checker spent time on.
Common fixes from the same wiki: explicit return types on exported functions, interfaces instead of large intersections, named types instead of huge inferred ones, project references to split a monorepo, and skipLibCheck.
TypeScript 6.0 changed types to default to [] for this reason. The 6.0 announcement says many projects gained 20 to 50% in build time by listing their @types packages. Upgrading to TypeScript 7 (question 25) is the other answer now.
Coding Exercises
23. Build an event emitter where the event name decides the payload type.
Map event names to payload types and index into that map with a generic key, so on and emit agree:
type EventMap = Record<string, unknown>;
class TypedEmitter<E extends EventMap> {
private handlers: { [K in keyof E]?: Array<(payload: E[K]) => void> } = {};
on<K extends keyof E>(type: K, fn: (payload: E[K]) => void): () => void {
(this.handlers[type] ??= []).push(fn);
return () => {
this.handlers[type] = this.handlers[type]?.filter((h) => h !== fn);
};
}
emit<K extends keyof E>(type: K, payload: E[K]): void {
for (const h of this.handlers[type] ?? []) h(payload);
}
}
type AppEvents = {
login: { userId: string };
logout: undefined;
};
const bus = new TypedEmitter<AppEvents>();
const off = bus.on("login", (p) => console.log(p.userId));
bus.emit("login", { userId: "u1" });
// @ts-expect-error payload needs userId
bus.emit("login", {});
// @ts-expect-error unknown event name
bus.on("signup", () => {});
off();
What to look for: the handler map is a mapped type, not Record<string, Function[]>; on returns an unsubscribe function; and AppEvents is a type alias, not an interface. That last point is a good probe.
An interface has no implicit index signature, so it does not satisfy Record<string, unknown>, and a candidate who hits that error should be able to explain it.
The runtime half of this exercise, without types, appears in the JavaScript coding challenges.
24. Type a get(obj, "a.b.c") helper so invalid paths fail to compile.
Generate the union of valid dot paths with a recursive mapped type, then resolve the value type with template literal inference:
type Path<T> = T extends object
? {
[K in keyof T & string]: T[K] extends readonly unknown[]
? K
: T[K] extends object
? K | `${K}.${Path<T[K]>}`
: K;
}[keyof T & string]
: never;
type PathValue<T, P extends string> =
P extends `${infer K}.${infer Rest}`
? K extends keyof T ? PathValue<T[K], Rest> : never
: P extends keyof T ? T[P] : never;
function get<T, P extends Path<T>>(obj: T, path: P): PathValue<T, P> {
let cur: unknown = obj;
for (const key of path.split(".")) cur = (cur as Record<string, unknown>)[key];
return cur as PathValue<T, P>;
}
const settings = { theme: { color: "dark", fontSize: 14 }, tags: ["a"] };
const size = get(settings, "theme.fontSize"); // number
const tags = get(settings, "tags"); // string[]
// @ts-expect-error not a valid path
get(settings, "theme.size");
The design choice to discuss is the array branch. This version stops at arrays rather than generating tags.0 paths, because index paths explode the union and slow the checker.
A senior candidate states that trade-off unprompted and keeps the runtime body simple, with the one cast at the return.
What Changed Recently
25. What is TypeScript 7, and what does adopting it involve?
It is the TypeScript compiler and language service ported to Go, announced in March 2025 as a native port under the codename Corsa.
The TypeScript 7.0 announcement (July 8, 2026) reports full-build speedups of 8x to 12x from native code and shared-memory multithreading.
It installs as the normal typescript package, version 7.0.2 is the current release, and the binary is tsc again. The preview's tsgo command name applied only up to the 7.0 RC.

The adoption catch is the API. TypeScript 7.0 ships without a programmatic API, and the team expects a new, different API in 7.1. Until then:
- Tools that import the compiler, such as typescript-eslint, need TypeScript 6.0. The team publishes
@typescript/typescript6, which installs atsc6binary and re-exports the 6.0 API, so the two can sit side by side through npm aliases. - Vue, MDX, Astro and Svelte projects should stay on 6.0 for now, and Angular projects can use 7.0 for command-line checks with 6.0 in the editor.
- The typescript-go repository was the staging repo for the port. It is now archived, and work continues in microsoft/TypeScript.
A strong candidate frames the upgrade as two steps: get clean on 6.0 with no ignoreDeprecations flag, then switch the build to 7.0.
26. Which tsconfig defaults changed in TypeScript 6.0, and what stops compiling in 7.0?
Per the 6.0 announcement (March 23, 2026), strict is true by default, module defaults to esnext, target floats to the newest supported ECMAScript version (es2025 at release), noUncheckedSideEffectImports is on, rootDir defaults to the folder holding tsconfig.json, and types defaults to [].
The last two cause the most confusion. Missing types shows up as errors such as "Cannot find name 'process'", fixed with "types": ["node"]. A missing rootDir shows up as output in dist/src/index.js instead of dist/index.js.
types default fails with hundreds of missing-name errors, and one that relied on the inferred rootDir builds cleanly into the wrong folder. Check both before reading any other error.TypeScript 6.0 deprecated a list of options, which "ignoreDeprecations": "6.0" could silence.
TypeScript 7.0 turns them into hard errors: target: es5, downlevelIteration, moduleResolution node/node10 and classic, module amd, umd, systemjs and none, baseUrl, and setting esModuleInterop to false.
Code that compiles cleanly on 6.0 without ignoreDeprecations, and with stableTypeOrdering on, should compile the same way on 7.0.
27. Node.js runs .ts files now. What does erasableSyntaxOnly do, and what still needs a build?
erasableSyntaxOnly, added in TypeScript 5.8 (February 28, 2025), makes the compiler reject any TypeScript syntax that has runtime behavior.
Those are exactly the constructs Node.js cannot strip: enums, namespaces with runtime code, class parameter properties, import = and export =, and angle-bracket type assertions.
The Node.js TypeScript docs recommend it together with verbatimModuleSyntax and rewriteRelativeImportExtensions.
interface Db { query(sql: string): Promise<unknown> }
// Fails under erasableSyntaxOnly: a parameter property emits code
class LegacyRepo {
constructor(private db: Db) {}
}
// Erasable equivalent
class Repo {
private db: Db;
constructor(db: Db) {
this.db = db;
}
}
Per the Node.js docs, some code still needs a build: .tsx files, tsconfig features such as paths, downleveling, and decorators. Node.js also refuses to strip TypeScript inside node_modules, so packages keep shipping JavaScript.
Node.js does no type checking either, so CI still runs tsc --noEmit. The runtime side, including which Node.js versions made stripping stable, is covered in the Node.js guide.
28. What is isolatedDeclarations, and why would a monorepo turn it on?
It requires exported functions, variables and class members to carry enough explicit types that a .d.ts file can be produced from each file alone, without running the type checker. It arrived in TypeScript 5.5.
// OK under isolatedDeclarations: the declaration file can be written from this file alone
export function add(a: number, b: number): number {
return a + b;
}
// Error under isolatedDeclarations: the return type must be inferred
export function sub(a: number, b: number) {
return a - b;
}
The 5.5 announcement explains the payoff. Tools other than tsc can generate declaration files. Projects that depend on each other no longer wait for a full type check of their dependencies before checking.
The 7.0 announcement notes the same exception for its --builders flag: project reference builds are limited by the dependency graph, except in codebases using isolatedDeclarations.
The cost is more annotations on exports, which the editor can add as quick fixes.
29. How does TypeScript 7 check in parallel, and why can type order differ from 6.0?
TypeScript 7.0 parses and emits files in parallel and runs a fixed pool of type-checker workers, 4 by default, set with --checkers. --builders sets how many project references build at once, and the two multiply: --checkers 4 --builders 4 allows 16 checkers. --singleThreaded turns parallelism off, which helps on small CI runners.
The announcement notes that changing --checkers can, in rare cases, surface order-dependent results, so a team may pin one value everywhere.
Ordering is the subtle part. Older compilers numbered types in the order they were met, so adding an unrelated const could flip a declaration file from 100 | 500 to 500 | 100.
Parallel checkers would make that random, so TypeScript 7 sorts types by content. TypeScript 6.0 added --stableTypeOrdering to imitate this, and the 6.0 post warns it can slow checking by up to 25%.
It is a migration aid, not a setting to keep. In 7.0 the behavior is always on.
30. Which code can type-check differently under TypeScript 7.0?
Beyond the deprecated options, the 7.0 announcement lists two areas. First, template literal inference now works on Unicode code points rather than UTF-16 code units:
type HeadTail<S> = S extends `${infer Head}${infer Tail}` ? [Head, Tail] : never;
type Result = HeadTail<"😀abc">;
// TypeScript 7.0: ["😀", "abc"]
// Before: ["\ud83d", "\ude00abc"]
Type-level string utilities that counted UTF-16 units, such as some Length helpers, change results.
Second, checking of JavaScript files with JSDoc was reworked to match .ts analysis. @enum is no longer special, Closure-style function types such as function(string): void are gone, and a value can no longer stand where a type is expected without typeof.
The team tracks the full list in the repository's CHANGES.md. A candidate who maintains a JSDoc-typed codebase should know to read it before upgrading.
Signs of a Strong Answer
- They treat every
as,anyand non-null!as a claim that needs a reason, and they push casts into one validating function. - They type external input as
unknownand validate it with a schema the static type is derived from. - They can explain why a conditional type distributed, or why a method parameter accepted an unsound argument, without guessing.
- They pick
moduleandmoduleResolutionfrom how the code is loaded, and know why bundler resolution is wrong for a published library. - They know the 6.0 default changes and the 7.0 API gap, and can plan an upgrade path that keeps typescript-eslint working.
- They measure a slow build with
--extendedDiagnosticsor a trace before changing code.
Hiring TypeScript Devs
Engineers who can design types for a real domain, and who know when to stop, are easier to judge with questions like these than with a résumé keyword.
Second Talent helps US startups hire vetted AI-Native Engineers from Asia, including TypeScript developers, matched in 24 hours at $29 to $39 USD an hour, billed monthly, with a 90-day one-time replacement guarantee.
See the cost to hire a TypeScript developer for rate details, or tell us what you need.





