Type-Safe Patterns
Applied designs that push checks to compile time — key-to-payload type registries, typed event emitters and WebSocket protocols, a fetch wrapper that infers by endpoint, plugin systems, generic repositories, typed Express middleware chains, and process.env.
12 questions
JuniorTheoryVery commonWhy is every read of process.env.API_URL typed string | undefined, and what is the safe pattern?
Why is every read of process.env.API_URL typed string | undefined, and what is the safe pattern?
process.env has an index signature over string | undefined: any name compiles, any read may be missing. Parse it once at startup, check required keys, export a typed config. Typing them string on ProcessEnv asserts them, deferring the crash.
Common mistakes
- ✗Augmenting
ProcessEnvto type keys asstring, which asserts rather than checks - ✗Reading environment variables ad hoc deep inside the code instead of once at startup
- ✗Expecting the compiler to know which variables the deployment actually sets
Follow-up questions
- →How would a schema validator improve on a hand-written check of each required key?
- →What breaks if two modules each read
process.envat import time in a different order?
JuniorTheoryVery commonHow do you type the request and response of a route handler in the web framework Express?
How do you type the request and response of a route handler in the web framework Express?
Express types the handler as RequestHandler<Params, ResBody, ReqBody, Query>: annotate those generic slots, not the callback's parameters. That types req.params, req.body and res.json. Nothing is checked at runtime — the body type is a claim.
Common mistakes
- ✗Expecting Express to infer
req.paramsfrom the route string - ✗Believing a typed
req.bodyis validated at runtime - ✗Annotating the callback parameters instead of the handler's generic slots
Follow-up questions
- →Where would you actually validate
req.bodyso the declared type stops being a lie? - →Why does the order of the type arguments in
Request<P, ResBody, ReqBody, Query>trip people up?
JuniorTheoryCommonHow do you declare an interface mapping names to payload types and index it with keyof?
How do you declare an interface mapping names to payload types and index it with keyof?
Declare an interface: the keys are the names, the property types are the payloads. keyof Events is the union of the names, so a parameter can be constrained to those keys, and Events[K] for K extends keyof Events yields that key's payload.
Common mistakes
- ✗Thinking
keyof Tyields the property types rather than the property names - ✗Falling back to
Record<string, unknown>, which erases the name-to-payload link - ✗Believing an interface cannot be indexed by a generic parameter
K extends keyof T
Follow-up questions
- →How would you make one key's payload optional so the caller may omit the argument entirely?
- →What does
Events[keyof Events]produce, and when is that union useful?
MiddleDesignCommonYour app has one shared emitter used by a dozen modules. Today on and emit take a string name and an any payload, so a renamed event or a payload that lost a field fails silently at runtime: the handler simply never runs, or runs with undefined where an id was expected. You want the event name restricted to the events that actually exist, and each handler's payload tied to the name it subscribed to, with no cast at any call site and no duplication between the list of names and the list of payload shapes. Describe how you would type the emitter, what the signatures of on and emit become, and what a caller sees when it emits a known name with the wrong payload.
Your app has one shared emitter used by a dozen modules. Today on and emit take a string name and an any payload, so a renamed event or a payload that lost a field fails silently at runtime: the handler simply never runs, or runs with undefined where an id was expected. You want the event name restricted to the events that actually exist, and each handler's payload tied to the name it subscribed to, with no cast at any call site and no duplication between the list of names and the list of payload shapes. Describe how you would type the emitter, what the signatures of on and emit become, and what a caller sees when it emits a known name with the wrong payload.
Make the emitter generic over a registry mapping each event name to its payload. on and emit become generic in K extends keyof E, taking name: K and payload E[K], so the name is restricted and a wrong payload errors at that argument.
Common mistakes
- ✗Keeping the names and the payload shapes in two declarations that drift apart
- ✗Typing the payload as a union of all shapes, forcing every handler to re-narrow it
- ✗Believing overloads are the only way to vary the payload type by event name
Follow-up questions
- →How would you type an event that carries no payload so
emit('close')needs no second argument? - →What changes if handlers may be asynchronous and
emitmust await them all?
MiddleCodeCommonLet a handler see the user that an auth middleware attached to the request
Let a handler see the user that an auth middleware attached to the request
Never augment the global Request. The injected property is an intersection, Request & { user: User }. withAuth returns a RequestHandler that attaches user and calls the handler with the narrowed request, so only it sees user, as required.
Common mistakes
- ✗Global-augmenting
Requestso every handler believesuserexists - ✗Making
useroptional and sprinkling non-null assertions in each handler - ✗Registering a handler that demands
AuthedRequestdirectly on the router
Follow-up questions
- →How would you compose two such wrappers so a handler sees both
userandtenant? - →Why does global augmentation weaken the guarantee even when the property is optional?
MiddleDesignCommonA service layer needs one repository implementation reused across a dozen entities — users, orders, invoices — each with its own row shape and its own id type. Today there is one Repository class per entity, all near-identical, and a fourteenth is due. You want a single repository whose findById, insert and update are checked against the specific entity: inserting an order row into the users repository, or filtering on a column that entity does not have, must not compile. Describe how you would type the repository across entities, what the method signatures look like, and where the entity's row shape enters the types.
A service layer needs one repository implementation reused across a dozen entities — users, orders, invoices — each with its own row shape and its own id type. Today there is one Repository class per entity, all near-identical, and a fourteenth is due. You want a single repository whose findById, insert and update are checked against the specific entity: inserting an order row into the users repository, or filtering on a column that entity does not have, must not compile. Describe how you would type the repository across entities, what the method signatures look like, and where the entity's row shape enters the types.
Key the repository on an entity registry — name → row type — and make it generic in K extends keyof Entities. Signatures derive from Entities[K]: the id type, the inserted row, the filter columns. A foreign row or unknown column will not compile.
Common mistakes
- ✗Parameterizing by the row type alone, leaving the table name an unchecked
string - ✗Typing filters as
Record<string, unknown>instead of deriving the columns from the row - ✗Believing one class cannot vary a method's parameter type per entity
Follow-up questions
- →How would you express that
insertreturns the row with its generatedidfilled in? - →What does this design cost when one entity needs a query the others do not have?
MiddleCodeCommonA fetch wrapper whose result type is inferred from the endpoint
A fetch wrapper whose result type is inferred from the endpoint
One route registry. request is generic in K extends keyof Routes, taking params: Routes[K]['params'] and returning Promise<Routes[K]['result']> — indexed accesses on the endpoint key, so the literal alone drives inference, with no assertion.
Common mistakes
- ✗Making the caller supply the result type explicitly instead of deriving it from the route
- ✗Reaching for overloads where one generic key parameter already varies both types
- ✗Believing a string-literal argument cannot drive a type parameter
Follow-up questions
- →How would you let a route with
params: voidbe called with one argument only? - →Where would you add runtime validation so
resultstops being an unchecked promise?
MiddleDesignOccasionalYou are adding a plugin system to a build tool. A plugin may implement any subset of the tool's hooks, and the hooks have genuinely different shapes: transform(code: string, id: string): string, resolve(spec: string): string | null, and done(stats: Stats): void. Third-party plugins are ordinary objects. You want a plugin author to get an error if a hook it implements has the wrong parameters or return type, and the host to iterate the registered handlers for one hook with each handler correctly typed — without a Record<string, Function> and without asserting inside the host. Describe how you would type the hook registry, the plugin object and the host's registration and dispatch.
You are adding a plugin system to a build tool. A plugin may implement any subset of the tool's hooks, and the hooks have genuinely different shapes: transform(code: string, id: string): string, resolve(spec: string): string | null, and done(stats: Stats): void. Third-party plugins are ordinary objects. You want a plugin author to get an error if a hook it implements has the wrong parameters or return type, and the host to iterate the registered handlers for one hook with each handler correctly typed — without a Record<string, Function> and without asserting inside the host. Describe how you would type the hook registry, the plugin object and the host's registration and dispatch.
A hook registry maps each name to its handler signature. A plugin is Partial<Hooks>: any subset, each hook checked against its signature. Registration and dispatch are generic in K extends keyof Hooks, so a hook's handlers are typed Hooks[K].
Common mistakes
- ✗Typing the hook table as
Record<string, Function>, which throws away every signature - ✗Requiring a nominal base class where a
Partialof the registry already checks each hook - ✗Asserting inside the host because dispatch was not made generic in the hook name
Follow-up questions
- →How would you let a hook be asynchronous in some plugins and synchronous in others?
- →What would change if a hook's result had to be piped into the next plugin's handler?
MiddleCodeOccasionalDerive a WebSocket message union and its handler map from one registry
Derive a WebSocket message union and its handler map from one registry
From the registry, a mapped type indexed by keyof yields the union { type: K } & Messages[K], and a second over the same keys the handler map, each taking its own message. dispatch switches on the discriminant; a new message fails to compile.
Common mistakes
- ✗Intersecting
keyofwith the union of payloads, which pairs every name with every shape - ✗Hand-writing the union beside the registry so the two drift apart
- ✗Assuming a mapped type cannot produce a union of object types
Follow-up questions
- →How does
dispatchbecome a compile error the moment a new message is added to the registry? - →How would you extend this so the server's and the client's message sets differ?
SeniorDesignOccasionalA request pipeline is assembled as a chain — chain.use(requestId).use(auth).use(tenant).handle(...). Each step attaches something to a shared context: requestId adds id, auth adds user, tenant adds org resolved from that user. Today the context is one interface with every key optional, so each step reads ctx.user!, and nothing stops tenant from being registered before auth: it compiles, and dies at runtime on undefined. You want every step to see, typed and required, exactly what the steps before it added and nothing else; the final handler to see the whole accumulation without anyone re-declaring it by hand; and the wrong order to be a compile error rather than a page at 3am. No global augmentation, no any, no assertions. Describe how you would type use and the chain, and what the handler ends up seeing.
A request pipeline is assembled as a chain — chain.use(requestId).use(auth).use(tenant).handle(...). Each step attaches something to a shared context: requestId adds id, auth adds user, tenant adds org resolved from that user. Today the context is one interface with every key optional, so each step reads ctx.user!, and nothing stops tenant from being registered before auth: it compiles, and dies at runtime on undefined. You want every step to see, typed and required, exactly what the steps before it added and nothing else; the final handler to see the whole accumulation without anyone re-declaring it by hand; and the wrong order to be a compile error rather than a page at 3am. No global augmentation, no any, no assertions. Describe how you would type use and the chain, and what the handler ends up seeing.
Thread the context through the type: the chain is generic in C, and use, generic in its step's addition, returns a chain of C & Extra. A step sees only the C it was registered on, so a later key will not compile; the handler gets it all.
Common mistakes
- ✗Declaring one context interface with every key optional, so each step reads
ctx.user! - ✗Believing a chain's type cannot accumulate what each successive step adds to it
- ✗Global-augmenting the request type, which lets a step read a key no earlier step added
Follow-up questions
- →How would you let a step declare that it requires
user, so registering it beforeauthfails? - →What happens to the accumulated type when a step attaches its key only on some requests?
SeniorDesignRareYour service layer is wired by hand and you are introducing a dependency-injection container. Two shapes are on the table. The first is class-based: services are classes, constructor parameters are decorated, and the container works out what to construct from the metadata the compiler flag emitDecoratorMetadata leaves behind and the reflection library reflect-metadata reads back. The second is function-based: a plain registry interface maps each token to the type it resolves to, and every service is registered as a factory closure that pulls its own dependencies out of the container. Both will run. Compare them on typing alone: what can the compiler actually prove in each, what happens when a dependency is an interface rather than a class, what does a get or resolve call return, and how does each behave when a token is never bound? Say which you would ship and what you give up.
Your service layer is wired by hand and you are introducing a dependency-injection container. Two shapes are on the table. The first is class-based: services are classes, constructor parameters are decorated, and the container works out what to construct from the metadata the compiler flag emitDecoratorMetadata leaves behind and the reflection library reflect-metadata reads back. The second is function-based: a plain registry interface maps each token to the type it resolves to, and every service is registered as a factory closure that pulls its own dependencies out of the container. Both will run. Compare them on typing alone: what can the compiler actually prove in each, what happens when a dependency is an interface rather than a class, what does a get or resolve call return, and how does each behave when a token is never bound? Say which you would ship and what you give up.
Reflection sees only class references: an interface erases and still needs a token, and get<T>(token) returns the T the caller named, unchecked. A registry makes resolve return S[K] and forces a factory per token, paid with explicit wiring.
Common mistakes
- ✗Believing decorator metadata can resolve an interface, which has no runtime value at all
- ✗Trusting
get<T>(token)to be checked, whenTis simply what the caller named - ✗Thinking a token-to-type registry cannot be indexed, so every resolution must be cast
Follow-up questions
- →How would you make the compiler reject a service whose factory forgets one of its own dependencies?
- →What breaks when one token must resolve differently per request rather than once as a singleton?
SeniorCodeRareCheck event names and their payloads at compile time in a subclass of EventEmitter
Check event names and their payloads at compile time in a subclass of EventEmitter
Make the subclass generic over a registry of name to argument tuple; override on and emit as generic in K extends keyof E: event: K, ...args: E[K]. An unknown name or wrong payload fails to compile; the one cast stays inside.
Common mistakes
- ✗Believing an override may not narrow the base
onandemitparameter types - ✗Typing one payload shape shared by every event instead of a tuple per name
- ✗Global-augmenting
EventEmitter, so every emitter in the process shares one event map
Follow-up questions
- →How do you keep
onceandoffin step, so a listener removed by reference still type-checks? - →What changes if a name maps to a single payload object instead of a tuple of arguments?