TypeScript
30 questions | entry to principal | answers written to be said out loud, not read
entry
What does it mean that TypeScript is structurally typed?
TypeScript compares shapes, not names. If two types have the same properties, they are compatible even if they were declared completely separately. This is different from nominal typing in languages like Java, where two classes with identical fields are still different types. It means I can pass a plain object anywhere an interface is expected, as long as the shape matches.
Explain any vs unknown vs never.
any turns off type checking completely, so I try to avoid it because it lets errors slip through silently. unknown also accepts anything, but it forces me to narrow the value before I can use it, so it is the safe version of any. never represents a value that can't happen, like the return type of a function that always throws, and I use it to make sure a switch statement handles every case.
What's the difference between a union and an intersection type?
A union with the pipe means a value can be one of several types, so I have to narrow before I can safely access anything specific to just one of them. An intersection with the ampersand combines multiple types into one that has to satisfy all of them at once. I use unions constantly for things like status fields, and intersections mostly for composing smaller object shapes together.
What are literal types and why are they useful?
A literal type narrows a primitive down to one specific value, so instead of just string I can say the type is exactly 'GET' or 'POST'. They're useful for modeling a fixed set of allowed values without reaching for an enum, and they combine naturally with unions to build something like a Method type. They also make autocomplete and typo catching much better than a plain string.
What are generics and why do you use them?
Generics let me write a function, type, or component that works across multiple types while keeping the relationship between the input and output type safe. Instead of typing a parameter as any, I give it a type parameter like T, and TypeScript infers what T actually is at the call site. The classic example is a function that returns the first element of an array, where the return type should match whatever array you passed in, not some fixed type.
Walk me through the built in utility types you reach for most, like Partial, Pick, Omit, and Record.
Partial makes every property optional, which I use a lot for update payloads where a caller only sends the fields they're changing. Pick and Omit both build a smaller type from an existing one, Pick by keeping a specific list of keys and Omit by dropping them. Record builds an object type from a union of keys mapped to a value type, which is perfect for something like a lookup table keyed by an id or a status.
junior
How does TypeScript narrow types, and what's a type guard?
Narrowing is TypeScript refining a broad type down to something more specific based on checks in my code, like typeof, instanceof, or an equality check. A type guard is a function that returns a boolean and tells the compiler that if it returns true, the value is a specific type. I write custom type guards with an is predicate when the built in checks aren't enough, like checking for a specific shape on an unknown API response.
What's a discriminated union and why do you like using them?
A discriminated union is a set of object types that all share a common literal field, like a kind or status property, that tells TypeScript which variant it's looking at. Once I check that field in an if or switch, TypeScript narrows the whole object automatically, so I get full type safety without a single cast. I use this pattern constantly for things like API responses or state machines with success and error branches.
What problem does the satisfies operator solve?
Before satisfies, I had to choose between annotating a variable with a type, which widens or loses the literal information, or leaving it untyped and losing error checking. satisfies checks that a value matches a type while still preserving the narrower, inferred type of the literal itself. It's great for a config object where I want validation against a shape but I still want autocomplete on the exact literal keys and values afterward.
What do readonly and const assertions actually give you?
readonly on a property or array stops me from reassigning it after the type is checked, though it's a compile time guarantee only and doesn't freeze anything at runtime. A const assertion, as const, goes further and tells TypeScript to infer the narrowest possible literal types for an object or array instead of widening them to string or number. I use as const a lot for things like a tuple of route names, where I want each value to keep its exact literal type.
What's a generic constraint and when do you need one?
A constraint uses extends to limit what a type parameter is allowed to be, so I can safely access properties on it inside the function. Without a constraint, T could be literally anything, so the compiler won't let me touch any property on it. I reach for this constantly, like constraining T to extend an object with an id field so a function can look values up by that id.
What do keyof and typeof do in the type system?
keyof takes an object type and gives me a union of property names as string literal types, which is great for functions that need to accept any valid key of that object. typeof, when used in a type position rather than a value position, captures the type of an existing variable or object so I don't have to redeclare it by hand. I combine them a lot, piping typeof on a config object into keyof to get a union of that config's actual keys.
What is an indexed access type?
It's using bracket notation on a type to pull out the type of one specific property, like User['id'], instead of retyping it separately. It keeps two types in sync automatically, because if the property's type changes on the source object, the indexed access type updates with it. I use it a lot with keyof to get the type of a dynamic property, like Config[keyof Config].
What about ReturnType, Awaited, Required, and Readonly?
ReturnType pulls out the return type of a function type, which saves me from duplicating a shape that's already defined somewhere else. Awaited unwraps a Promise, including nested ones, so I can describe what a value actually resolves to. Required is the opposite of Partial and forces every property to be present, and Readonly locks every property against reassignment at the type level. I reach for these instead of hand rolling the same conditional or mapped type myself.
What does strictNullChecks change about how you write code?
Without it, null and undefined are quietly assignable to almost anything, so a lot of real bugs get hidden. With it on, null and undefined become their own distinct types, so if a function might return nothing, I have to actually handle that case before I can use the result. It pushes me to be explicit about optional values with a question mark or a union, instead of finding out about a missing value in production.
How do you use generics with React component props?
I put a generic type parameter on the component function itself when the component needs to stay flexible about the shape of its data, like a List component that renders any array of T and gives back a typed T in its renderItem callback. This keeps the caller getting full autocomplete on their own data type instead of me hardcoding a specific shape or falling back to any. It's the same pattern as a generic function, just applied to a component.
senior
Type vs interface, when do you reach for each?
Both describe the shape of an object and in most cases they are interchangeable. I reach for interface when I am describing an object shape that might be extended or implemented by a class, because interfaces support declaration merging and read a little closer to OOP. I reach for type when I need unions, intersections, tuples, or mapped types, since those are not expressible with interface alone. On a team I just pick one convention and stay consistent.
Enums or a union of string literals, which do you prefer?
I lean toward a union of string literals for most cases. A regular enum generates actual runtime code and has some quirky behavior with numeric values, while a literal union is purely a compile time construct with zero runtime cost. Enums do give you a namespace and can be nicer for iteration, but for something like a status field a literal union plus a readonly array of the values covers what I need.
What's the difference between a type assertion and the non-null assertion, and why be careful with both?
A type assertion with as tells the compiler to trust me about a value's type without any runtime check, which is different from casting in languages that actually convert the value. The non-null assertion, the exclamation mark, tells the compiler a value isn't null or undefined even though its type says it might be. Both are escape hatches, and I only use them when I have information the compiler genuinely can't infer, because if I'm wrong they fail at runtime instead of at compile time.
What are conditional types and what are they good for?
A conditional type picks between two types based on a check, written as T extends U ? X : Y, which is basically an if statement at the type level. They're good for building utility types that behave differently depending on the shape of the input, like extracting the element type out of an array or unwrapping a Promise. Most of the built in utility types like Exclude and NonNullable are actually implemented as conditional types.
What's a mapped type?
A mapped type builds a new type by iterating over the keys of an existing one and transforming each property, using a syntax like [K in keyof T]. This is exactly how Partial, Readonly, and Pick are implemented under the hood. I write my own mapped types when I need a transformation the built in utilities don't cover, like making every property in an object nullable.
What are template literal types used for?
They let me build string types the same way you'd build a template string, combining literals with unions to generate every valid combination. A common use is modeling event names, like combining 'on' with 'Click' or 'Hover' to get a union of onClick and onHover. I've also used them to strongly type route paths and CSS in JS style property names.
What does the infer keyword do inside a conditional type?
infer lets me capture a type from somewhere inside a larger type and bind it to a new type variable, right in the middle of a conditional type check. It's how I pull the return type out of a function type or the resolved value out of a Promise without knowing what that type is ahead of time. ReturnType and Awaited are both built using infer internally.
How do you type asynchronous code, like an async function or a fetch call?
An async function's return type always gets wrapped in a Promise automatically, so I type the resolved value and let TypeScript add the Promise wrapper. For something like fetch, the response body comes back as unknown or any depending on how it's parsed, so I validate or assert the shape right at that boundary rather than trusting it blindly. I also make sure error branches are typed, since a rejected promise's reason is typed as unknown by default in strict mode.
What's a .d.ts file for?
A declaration file describes types without containing any actual implementation, so it lets TypeScript understand a piece of JavaScript that has no types of its own, like an older untyped library. I write one when I'm consuming a plain JS module, or when I'm publishing a package and want to ship type information separately from the compiled output. DefinitelyTyped is the community project that maintains these for thousands of packages that don't ship their own.
What's a type only import and why would you use one?
Writing import type tells the compiler that an import is only used for type checking and should be completely erased from the compiled output. This matters when a bundler processes files in isolation, because it can't always tell on its own whether an import is a real value or just a type, and removing that ambiguity avoids pulling in unnecessary runtime code or breaking circular type only imports. I reach for it whenever I'm importing something purely to annotate a parameter or return type.
staff
What is declaration merging and how does module augmentation use it?
Declaration merging is TypeScript combining multiple declarations that share the same name into one, which works for interfaces but not for type aliases. Module augmentation uses this to let me add properties to a type that lives in a library I don't own, like adding a custom field to Express's Request object. I use it sparingly because it's global and implicit, so I try to keep it in one clearly named file so the team can find it.
What does the strict flag in tsconfig actually turn on?
strict is a bundle of several individual flags, not one setting, things like strictNullChecks, noImplicitAny, strictFunctionTypes, and strictPropertyInitialization. Turning it on all at once on an existing loose codebase usually produces a wave of errors, so on a migration I sometimes enable the individual flags one at a time so the work is reviewable. On a new project I always start with strict true from day one because retrofitting it later is much more painful.
Types disappear at compile time, so how do you actually validate data at runtime?
TypeScript types are fully erased during compilation, they exist purely to catch mistakes while I'm writing code and have zero presence in the JavaScript that actually runs. That means an external input, like a JSON body from an API or a form submission, isn't actually checked against my type at runtime just because I annotated it. For anything crossing a real boundary I use a runtime validation library like Zod, which both validates the shape and can infer the exact same TypeScript type from that one schema.
How would you approach migrating a large JavaScript codebase to TypeScript?
I start by renaming files to .ts with allowJs and checkJs on and strict mode off, so the project keeps compiling while I bring types in incrementally rather than stopping the world. I convert file by file, usually starting from the leaves like utilities and shared types before moving into components and routes that depend on them. Once most of the codebase is converted I turn strict flags on one at a time, fix what surfaces, and treat any as a temporary marker I track down and remove rather than a permanent escape hatch.
Fast recall
unknown = safe any, must narrow | never = value that can't happen | satisfies = validates without widening | keyof = union of property names | infer = capture a type inline | as const = narrowest literal types | Partial = every property optional | Pick = keep a subset of keys | Omit = drop a subset of keys | Record = keyed lookup object | ReturnType = function's return type | Awaited = unwraps a promise | import type = erased at compile time