JavaScript and TypeScript
The front-end language from the inside: types and equality, scope and this, the execution model with the event loop and promises, and TypeScript from the basics to advanced types. This is where a back-end developer gets tested in a fullstack interview: the places where JavaScript behaves differently from C#.
What types does JavaScript have, and how is == different from ===?
Section titled “What types does JavaScript have, and how is == different from ===?”What it is
- Primitives —
string,number,bigint,boolean,undefined,null,symbol. Immutable, compared by value. - Objects — everything else:
{}, arrays, functions,Date. A variable holds a reference, like a class-typed variable in C#.
How to use
- Check the type with
typeof:typeof 'a'is'string'. Two oddities:typeof nullis'object', and for a function it is'function'. Check for arrays withArray.isArray. - Compare with
===: both type and value must match.==converts types before comparing, so0 == ''istrue. - Falsy values:
false,0,'',null,undefined,NaN. Everything else counts as true in anif, including[]and'0'.
Common mistake
Writing count || 10 for a default. With count = 0 you get 10; you need count ?? 10.
Takeaway: primitives are compared by value, objects by reference, and you compare with ===.
How it works
===is strict equality: different types givefalseimmediately, and objects are equal only if they are the same reference.==is loose equality:nullandundefinedequal only each other, a string or abooleanis converted to a number, and an object to a primitive viavalueOfortoString.NaNequals nothing, not even itself. Check it withNumber.isNaN(x).
When to choose what
| Task | What to write |
|---|---|
| Compare values | === |
Check for null and undefined at once |
x == null |
Default only for null and undefined |
x ?? def |
| Default for any falsy value | x || def |
Find NaN in an array |
includes, not indexOf |
Pitfalls
typeof null === 'object'. The “is it an object” check istypeof x === 'object' && x !== null.- Global
isNaN. It converts its argument to a number first:isNaN('abc')istrue. UseNumber.isNaN. - Comparing objects.
{ id: 1 } === { id: 1 }isfalse. There is noEqualsand no operator overloading as in C#: deep comparison is hand-written or taken from a library. - Fractions.
numberis a 64-bit double, likedoublein C#:0.1 + 0.2 !== 0.3. Money is counted in cents or sent as a string.
Rule of thumb
Always ===, except x == null; for defaults, ??. The ESLint eqeqeq rule enforces this in code.
Takeaway: == hides type coercion, so compare with === and set defaults with ??.
Under the hood
- Per the spec,
==is the IsLooselyEqual algorithm: when types differ, the operands are converted step by step to a number or a primitive. An object becomes a primitive viaSymbol.toPrimitive,valueOfortoString, hence[] == false:[]→''→0. typeof null === 'object'is a bug from the first implementation of the language: the “object” type tag was zero, and so was thenullpointer. It was kept for compatibility.- There are four kinds of equality:
==,===,Object.is(SameValue) and SameValueZero. The last one is used byincludes,MapandSet, which is why they findNaN.
Cost
numberis an IEEE 754 double. Integers are exact up toNumber.MAX_SAFE_INTEGER(2^53 − 1).JSON.parsesilently rounds a C#longID above that limit:9007199254740993becomes9007199254740992.bigintis exact, but it does not mix withnumber(1n + 1is aTypeError), andJSON.stringifycannot serialize it.
Trade-offs
- The back end sends large
longanddecimalvalues as strings: in System.Text.Json viaJsonNumberHandling.WriteAsStringor a converter on specific fields. The alternative is agreeing that IDs never exceed 2^53. MapandSetcompare keys by reference. Look an object key up by the same instance, or switch to a string key.
How to check in production
- ESLint:
eqeqeq, and for TypeScript@typescript-eslint/prefer-nullish-coalescingandstrict-boolean-expressions. - Lost ID precision is caught by an integration test with an ID above 2^53: it must come back unchanged in the response.
Takeaway: equality in JavaScript is several algorithms with different semantics, and the most expensive trap at the .NET boundary is number precision.
console.log(typeof null); // object — a historical bugconsole.log(typeof undefined); // undefinedconsole.log(typeof (() => {})); // function
console.log(0 == ''); // true: '' is converted to 0console.log(0 === ''); // false: different typesconsole.log(null == undefined); // trueconsole.log(null == 0); // falseconsole.log([] == false); // true: [] → '' → 0, false → 0console.log(Boolean([])); // true: every object is truthy
console.log(NaN === NaN); // falseconsole.log(Number.isNaN(NaN)); // trueconsole.log([NaN].includes(NaN)); // trueconsole.log([NaN].indexOf(NaN)); // -1
const qty = 0;console.log(qty || 10); // 10: 0 is falsyconsole.log(qty ?? 10); // 0: 0 is neither null nor undefined
const a = { id: 1 };const b = a;console.log(a === b); // true: same referenceconsole.log(a === { id: 1 }); // false: a different object- How is
undefineddifferent fromnull? —undefinedmeans “no value set”: an uninitialized variable, a missing property;nullis an explicit “empty”. - When is
==acceptable? —x == null: one check for bothnullandundefined. - How is
Object.isdifferent from===? —Object.is(NaN, NaN)istrue,Object.is(0, -0)isfalse. - Why is
typeof null'object'? — A bug in the first implementation, kept for compatibility. - How do you pass a C#
longwithout losing precision? — As a string;numberis exact only up to 2^53 − 1.
How do scope, closures and this work?
Section titled “How do scope, closures and this work?”What it is
- Scope is the part of the code where a variable is available.
letandconstare visible in a{ }block,varacross the whole function. - A closure is a function together with the variables available when it was created. It still sees them after the outer function returns.
thisis the object “before the dot” in a call: inuser.hello()it isuser.
How to use
- Declare variables with
const, and withletwhen the value changes. New code does not needvar. - A closure holds private state:
makeCounter()returns a function with its owncount. - Write callbacks as arrow functions: they have no
thisof their own and take it from outside.
Common mistake
Passing a method as a callback: setTimeout(user.hello, 100). The function is not called through user, so this no longer points to it. The fix is setTimeout(() => user.hello(), 100).
Takeaway: scope decides where a variable is visible, a closure decides how long it lives, and this is the object the function was called on.
How it works
| Property | var |
let, const |
|---|---|---|
| Scope | function | block |
| Before declaration | undefined |
ReferenceError (TDZ) |
| Redeclaration | allowed | error |
Rules for this, by priority:
new F()— the new object.f.call(obj),f.apply(obj, args),f.bind(obj)— the object passed explicitly.bindreturns a new function withthisfixed.obj.f()— the object before the dot.f()—undefinedin strict mode (modules and classes are always strict), otherwise the global object.
Arrow functions are outside these rules: they take this from the surrounding scope at creation, and call or bind do not change it.
Pitfalls
- A
varloop. All callbacks share one variable and see its last value.letin aforcreates a new variable per iteration. In C#,forbehaves the same way, whileforeachhas created a new variable per iteration since C# 5. - Detaching a method.
const f = obj.methodlosesobj. In C#, a delegateAction a = obj.Methodremembers the object; in JavaScript it does not. - An arrow as an object method. An arrow function in an object literal takes
thisfrom outside, not the object itself.
Rule of thumb
const by default, let when you need to reassign. Callbacks as arrow functions; a method you hand out is an arrow field or bound with bind.
Takeaway: scope and closures are lexical — the place in the code decides — while this is dynamic — the call site decides.
Under the hood
- Per the spec, a function keeps a reference to the lexical environment (Environment Record) where it was created. Variable lookup walks the chain of environments outward.
- The engine puts captured variables into a context object on the heap. In V8, closures created in the same function share one context: a long-lived callback also keeps variables that only a sibling closure needs.
thisis not an environment variable but a call parameter. Arrow functions takethis,argumentsandsuperfrom the enclosing function, which is why they cannot be called withnew.
Cost
- A class method is one function on the prototype. An arrow field
handle = () => {}is a separate function in every instance: memory per object, and a subclass cannot call it viasuper.handle(). - A closure retains everything it captured. An event handler or timer that is never removed keeps a component and its data alive — a common leak in SPAs.
Trade-offs
bindor an arrow field: memory per instance versus a reliablethiswhen a method is handed out.- A closure gives real privacy without classes. Private fields
#xgive it to a class too, without a separate function per instance.
How to check in production
- Chrome DevTools → Memory → Heap snapshot: compare two snapshots and follow the Retainers chain.
(closure)andsystem / Contextentries show which closure holds the memory. - ESLint
no-var,prefer-constandno-loop-funcremove thevarand loop traps.
Takeaway: a closure is an environment on the heap that lives as long as the function, and this is set by the call; both are common sources of leaks and callback bugs.
console.log(v); // undefined: var is hoisted and set to undefinedvar v = 1;try { console.log(l);} catch (e) { console.log(e.name); // ReferenceError: l is in the TDZ}let l = 2;
const withVar = [];for (var i = 0; i < 3; i++) withVar.push(() => i);console.log(withVar.map(f => f())); // [ 3, 3, 3 ]: one i for the whole function
const withLet = [];for (let j = 0; j < 3; j++) withLet.push(() => j);console.log(withLet.map(f => f())); // [ 0, 1, 2 ]: a new j per iteration
function makeCounter() { let count = 0; // lives as long as the returned function return () => ++count;}const next = makeCounter();next();console.log(next()); // 2class Counter { count = 0; inc() { return ++this.count; } // method on the prototype incArrow = () => ++this.count; // field: a separate function per object}
const c = new Counter();const inc = c.inc; // method detached from the objectconst incArrow = c.incArrow;
try { inc(); // this === undefined: classes are strict} catch (e) { console.log(e.name); // TypeError}console.log(incArrow()); // 1: this taken from the constructorconsole.log(inc.call(c)); // 2: this set explicitlyconsole.log(inc.bind(c)()); // 3: bind returns a new functionconsole.log(inc.apply(c, [])); // 4: like call, arguments as an array- What does a loop with
varandsetTimeoutprint, and how do you fix it? — Three identical values; replacevarwithletor pass the value as a parameter. - Can you change an arrow function’s
thiswithbind? — No,bind,callandapplyignore it. - How is
calldifferent fromapply? — Only in how arguments are passed: as a list or as an array. - What is the TDZ? — The region from the start of a block to a
letorconstdeclaration where access throwsReferenceError. - Why does a handler passed to
addEventListenerlose the object’sthis? — The browser calls it, andthisbecomes the element the event fired on.
How do the event loop, promises and async/await work in JavaScript?
Section titled “How do the event loop, promises and async/await work in JavaScript?”What it is
- One thread. While a function runs, nothing else happens.
- The event loop is a loop that takes the next task from the queue once the current code has finished: a click, a timer, a server response.
- A promise is an object that will later be fulfilled with a result or rejected with an error.
How to use
- An
asyncfunction always returns a promise. awaitinside it waits for a promise and gives you its result; the thread is free meanwhile.- Errors are caught with a regular
try/catcharoundawait. - Independent requests run in parallel:
await Promise.all([loadUser(), loadOrders()]).
Common mistake
Calling an async function without await and without .catch. An error inside it will not reach your try/catch and becomes unhandled.
Takeaway: one thread plus a task queue, and promises with await let you wait without blocking that thread.
How it works
- Synchronous code runs on the call stack to completion.
- When the stack is empty, all microtasks run:
then,catchandfinallycallbacks, continuations afterawait,queueMicrotask. Microtasks added along the way run in the same pass. - The browser may render a frame.
- The event loop takes one task from the queue:
setTimeout, an event, a network message. The loop repeats.
The code of an async function up to the first await runs synchronously; the rest becomes a microtask.
When to choose what
| Method | Result |
|---|---|
Promise.all |
array of results; rejects on the first error |
Promise.allSettled |
the status of every promise, never rejects |
Promise.race |
the first to settle — success or error |
Promise.any |
the first success; if all fail — AggregateError |
Pitfalls
Promise.alldoes not cancel the rest. When one promise rejects, the other requests keep running; their results are simply lost.forEachwith anasynccallback.forEachdoes not wait for promises. Sequentially —for...ofwithawait; in parallel —Promise.allwithmap.- Needless sequencing. Two independent
awaits in a row wait for the sum of the times, not the maximum.
Rule of thumb
Every promise is either awaited, returned, or explicitly handled with .catch. Independent operations go through Promise.all.
Takeaway: microtasks run before the next task, and the promise combinators differ in how they react to errors.
Under the hood
- The ECMAScript spec describes promise reactions as jobs, and the browser and Node.js put them into the microtask queue. Node.js also has the
process.nextTickqueue, which runs before promises, and the libuv loop phases: timers, I/O,setImmediate. - An
asyncfunction is a state machine, as in C#. But the continuation afterawaitalways goes through the microtask queue, even if the promise is already fulfilled; in C#,awaiton a completedTaskcontinues synchronously. - I/O is done by the browser and libuv on their own threads; only the callbacks come back into JavaScript.
Cost
- Everything is on one thread. A 200 ms computation freezes the UI or, on Node.js, every server request. CPU work goes to a Web Worker or
worker_threads. - There are no locks, but there are races between
awaits: the responses to two search requests arrive out of order, and the old one overwrites the new one.
Trade-offs
- Promises cannot be cancelled. Cancellation is passed via
AbortControllerandAbortSignal— the counterpart ofCancellationToken:fetch(url, { signal }),AbortSignal.timeout(5000). Promise.allis fail-fast, which is usually what you want.allSettledis for when a partial result beats none.
How to check in production
- Unhandled rejections: in the browser, send the
unhandledrejectionevent to error monitoring; Node.js 15+ crashes by default. - Long tasks show up in DevTools → Performance. Event loop delay in Node.js is measured with
perf_hooks.monitorEventLoopDelay(). - The
@typescript-eslint/no-floating-promisesrule finds unhandled promises.
Takeaway: one thread and the order “task → all microtasks → frame” give a simple model without locks, but any long synchronous work stops everything.
console.log('A'); // 1setTimeout(() => console.log('timeout'), 0); // 7: taskPromise.resolve().then(() => console.log('then')); // 4: microtaskqueueMicrotask(() => console.log('microtask')); // 5: microtask
async function run() { console.log('run start'); // 2: before await — synchronous await null; console.log('after await'); // 6: continuation — microtask}run();console.log('B'); // 3The numbers in the comments are the output order.
// ES module: top-level awaitconst ok = (value, ms) => new Promise(resolve => setTimeout(() => resolve(value), ms));const fail = (message, ms) => new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
try { await Promise.all([ok(1, 50), fail('boom', 10)]);} catch (e) { console.log(e.message); // boom: after 10 ms, not waiting for ok(1)}
const settled = await Promise.allSettled([ok(1, 10), fail('boom', 10)]);console.log(settled.map(r => r.status)); // [ 'fulfilled', 'rejected' ]
console.log(await Promise.any([fail('a', 5), ok(2, 20)])); // 2: first successawait Promise.race([fail('a', 5), ok(2, 20)]) .catch(e => console.log(e.message)); // a: first to settle
try { await Promise.any([fail('a', 5), fail('b', 5)]);} catch (e) { console.log(e.constructor.name, e.errors.length); // AggregateError 2}- Which runs first:
setTimeout(fn, 0)orPromise.resolve().then(fn)? —then: microtasks run before the next task. - How is
Promise.alldifferent fromTask.WhenAll? —Promise.allrejects as soon as the first error occurs;WhenAllwaits for all tasks. - How do you cancel a
fetch? — Pass thesignalof anAbortControllerand callabort(). - Why does an endless chain of microtasks hang the page? — The microtask queue is drained completely before rendering and the next task.
- Does JavaScript need locks? — No, but races between
awaits are possible; they are handled by cancelling stale requests.
What does TypeScript give you, and how do you use it properly?
Section titled “What does TypeScript give you, and how do you use it properly?”What it is
TypeScript is a layer on top of JavaScript: the same language plus type annotations. The tsc compiler checks the types and emits JavaScript without them.
Why
- Errors are found at build time: a property typo, a missing argument, a possible
undefined. - The IDE knows the types: autocompletion and safe renaming.
How to use
- Describe the shape of data with
interfaceortype:interface User { id: number; name: string }. - Annotate function parameters; the compiler often infers variable types itself:
const n = 5is anumber. - Keep
"strict": trueintsconfig.json; since TypeScript 6.0 this is the default.
Common mistake
Reaching for any when the compiler complains. any switches checking off, and the error moves to runtime. For a value of unknown type there is unknown.
Takeaway: TypeScript catches errors before the code runs, but at runtime the types are gone.
How it works
- Structural typing. An object fits
interface Pointif it hasxandy. C# typing is nominal: two classes with the same fields are incompatible. - Narrowing. The compiler tracks checks: after
typeof x === 'string',xhas typestring. This works withtypeof,instanceof,in,===and a type guard — a function returningx is User. - Generics look like C#:
function first<T>(items: T[]), constraints viaextends. ButTdoes not exist at runtime:new T()andtypeof Tare impossible.
When to choose what
| Task | Choice |
|---|---|
| Object shape, class contract | interface, extended via extends |
| Union, tuple, computed type | type |
| Value of unknown type | unknown plus narrowing |
| Migrating from JavaScript, no other way | any, sparingly |
Pitfalls
- Excess property checks apply only to object literals. The same object passed through a variable gets through.
asis not a cast.data as Userchecks nothing at runtime.strictis not everything. Index accessarr[0]givesT, notT | undefined, untilnoUncheckedIndexedAccessis on.
Rule of thumb
strict always, unknown instead of any, and as only where you know more than the compiler — with a comment saying why.
Takeaway: TypeScript compares types by shape and refines them from checks in the code, while strict and unknown keep those checks from being bypassed.
Under the hood
tscis type checking plus JavaScript emit. The esbuild and SWC bundlers, as well as Node.js 22.18+, only strip types without checking them, sotsc --noEmitis a separate CI step.- Not everything can be stripped:
enum,namespacewith code and parameter properties produce JavaScript. TheerasableSyntaxOnlyflag (TypeScript 5.8) forbids such syntax. - Types do not exist at runtime, so DI in Angular and NestJS relies on classes as values. An interface cannot be a token: you create an
InjectionTokenor a string token for it.
Cost
- An API response is typed only as far as you choose to trust it:
res.json()returnsPromise<any>. A format error surfaces far from where the data arrived. - Runtime schema validation costs CPU and bundle size, but the error fails at the boundary with a clear path to the field.
Trade-offs
- Zod (or Valibot, ArkType): the schema is the single source of truth, and the type is inferred with
z.infer. Generating types from OpenAPI gives a contract without runtime validation. - Structural typing confuses
UserIdandOrderIdif both arestring. A branded typestring & { readonly __brand: 'UserId' }brings nominal typing back. - TypeScript 7 (July 2026) is the compiler rewritten in Go: type checking is roughly 10 times faster.
How to check in production
- In CI:
tsc --noEmitand ESLint with@typescript-eslint/no-explicit-anyand theno-unsafe-*rules. - Schema validation errors are logged with the field path: they are the first sign that the back end changed the contract.
Takeaway: TypeScript checks code, not data, so the boundary with the outside world needs runtime validation.
interface Point { x: number; y: number }class Vector { x = 3; y = 4; } // knows nothing about Point
function length(p: Point): number { return Math.hypot(p.x, p.y);}console.log(length(new Vector())); // 5: fits by shapeconsole.log(length({ x: 6, y: 8 })); // 10
function describe(value: unknown): string { if (typeof value === 'string') return value.toUpperCase(); // value: string if (Array.isArray(value)) return `array of ${value.length}`; // value: any[] return 'other';}console.log(describe('ts')); // TSconsole.log(describe([1, 2])); // array of 2
function first<T>(items: T[]): T | undefined { return items[0];}const n = first([1, 2, 3]); // n: number | undefinedconsole.log(n?.toFixed(1)); // 1.0// npm i zodimport { z } from 'zod';
const User = z.object({ id: z.number(), name: z.string() });type User = z.infer<typeof User>; // { id: number; name: string }
const raw: unknown = JSON.parse('{"id":"42","name":"Ann"}');
const trusted = raw as User; // compiles: as checks nothingconsole.log(typeof trusted.id); // string
const result = User.safeParse(raw);if (result.success) { console.log(result.data.id);} else { console.log(result.error.issues[0].path); // [ 'id' ]}- How is
typedifferent frominterface? —interfacecan be extended and merges when declared twice;typedescribes unions, tuples and computed types. - How is
unknowndifferent fromany? — You can do nothing withunknownwithout narrowing, whileanyswitches checking off and spreads through the code. - What does
strictinclude? —strictNullChecks,noImplicitAny,strictFunctionTypes,strictPropertyInitialization,useUnknownInCatchVariablesand more. - Why can’t you write
new T()? — Types are erased; pass the constructor as a parameterctor: new () => T. - How do you make a nominal type for an ID? — A branded type: an intersection of
stringwith a unique marker field.
Advanced TypeScript types: discriminated unions, utility, mapped and conditional types — when do you need them?
Section titled “Advanced TypeScript types: discriminated unions, utility, mapped and conditional types — when do you need them?”What it is
- A union
A | Bis a value of one of the types. A discriminated union is a union of objects with a shared field that holds its own literal value in each variant:status: 'draft',status: 'paid'. - Utility types are built-in “functions on types”: they take a type and return a new one.
How to use
- Check the tag field in an
iforswitch— inside the branch TypeScript knows the exact variant: in the'paid'branch,paidAtis available. Partial<T>— all fields optional: the body of a PATCH request.Pick<T, 'a' | 'b'>andOmit<T, 'id'>— keep or drop fields: a create form withoutid.Record<K, V>— a dictionary:Record<string, number>.
Common mistake
Describing state with optional fields: { status: string; paidAt?: string }. The compiler does not know when paidAt is set, and checks spread across the code.
Takeaway: a discriminated union describes the variants, and utility types derive new types from existing ones without copying.
How it works
keyof Tis a union of property names,T[K]is the type of a property by key,typeof valueis the type of a value.- A mapped type
{ [K in keyof T]: ... }iterates over keys and builds a new object type. The modifiers?,readonlyand-?add or remove flags, andasrenames keys. This is howPartial,ReadonlyandRecordare built. - A conditional type
T extends U ? X : Ypicks a type by condition.infer Rextracts part of a type:T extends Promise<infer R> ? R : T.ReturnType,AwaitedandExcludeare built on this. - The
nevercheck. In thedefaultbranch after all thecases, the type is narrowed tonever. The lineconst x: never = orderstops compiling as soon as a new variant appears. satisfies(TypeScript 4.9) checks that a value fits a type but keeps the inferred type:keyof typeof routesis'home' | 'user', notstring. Angular 13 projects (TypeScript 4.4–4.6) did not have it yet.
Pitfalls
Omit<User, 'idd'>with a typo compiles:Omitdoes not check its keys, whilePickdoes.Partialis shallow: nested objects stay required.- A conditional type over a union distributes across its members:
ToArray<string | number>givesstring[] | number[]. To prevent this, wrap both sides in a tuple:[T] extends [U].
Rule of thumb
States — a discriminated union with a never check; derived shapes — utility types from one source type; your own mapped and conditional types — only when the built-in ones fall short.
Takeaway: keyof, mapped and conditional types are the parts utility types are built from, and never turns a forgotten variant into a compile error.
Under the hood
- These types exist only for the compiler and emit no code. Every application of a generic type is an instantiation that the compiler computes and caches.
- Recursion has a limit: instantiation that is too deep fails with TS2589, “Type instantiation is excessively deep and possibly infinite”.
Cost
Complex generic types slow down type checking and IDE hints, and error messages unfold over several screens.
Trade-offs
- Generating from OpenAPI. ASP.NET Core (
Microsoft.AspNetCore.OpenApi, .NET 9+) produces the document,openapi-typescriptgenerates types from it, and NSwag or Orval generate a full client. The types never drift from the back end, but they are only as accurate as the API description: without[ProducesResponseType]orTypedResultsthe response schema is incomplete. - Generated versus your own. Generated DTOs are transport. Domain states on the client are better described as a discriminated union, with the response mapped into them.
- Expressiveness versus readability. A 20-line type built from conditional and mapped types is clear only to its author. Keep the public contracts of modules simple.
How to check in production
tsc --extendedDiagnosticsshows the instantiation count and check time, and--generateTraceshows which types are expensive.- Type tests:
@ts-expect-errororexpectTypeOfin Vitest pin down how your own utilities behave. - CI regenerates types from OpenAPI and fails on
git diff --exit-codeif the contract changed without the front end.
Takeaway: advanced types pay off at boundaries — states and API contracts — and you pay for them in compile time and readability.
type Order = | { status: 'draft' } | { status: 'paid'; paidAt: string } | { status: 'shipped'; trackingNo: string };
function label(order: Order): string { switch (order.status) { case 'draft': return 'Draft'; case 'paid': return `Paid at ${order.paidAt}`; // order narrowed to the 'paid' variant case 'shipped': return `Track ${order.trackingNo}`; default: { const unreachable: never = order; // a new status — compile error here return unreachable; } }}
console.log(label({ status: 'paid', paidAt: '2026-10-10' })); // Paid at 2026-10-10console.log(label({ status: 'shipped', trackingNo: 'UA1' })); // Track UA1interface User { id: number; name: string; email: string }
type UserPatch = Partial<Omit<User, 'id'>>; // { name?: string; email?: string }type UserById = Record<number, User>; // dictionary id → User
type Getters<T> = { [K in keyof T & string as `get${Capitalize<K>}`]: () => T[K];};type NameGetter = Getters<Pick<User, 'name'>>; // { getName: () => string }
type Unwrap<T> = T extends Promise<infer R> ? R : T;type Loaded = Unwrap<Promise<User[]>>; // User[]
const routes = { home: '/', user: '/users/:id',} satisfies Record<string, string>;type RouteName = keyof typeof routes; // 'home' | 'user'
const patch: UserPatch = { name: 'Ann' };console.log(Object.keys(patch)); // [ 'name' ]console.log(routes.user); // /users/:idWith the annotation const routes: Record<string, string>, RouteName would be just string.
- How is a union of strings different from an
enum? — A union is erased without a trace, anenumemits an object in JavaScript; new code usually prefers the union. - Why
satisfiesif you can annotate the type? — An annotation widens the type to the declared one;satisfieschecks and keeps the precise inference. - What does
inferdo? — It declares a type variable inside the condition of a conditional type and extracts part of the type into it. - How do you guarantee a
switchhandles every variant? — Assign the value to a variable of typeneverin thedefaultbranch. - Where should back-end DTO types come from? — Generate them from OpenAPI in CI instead of writing them by hand.