37 questions
TypeScript interview questions and answers
Junior questions
1. What does TypeScript add to JavaScript?
Answer
TypeScript adds static types for variables, functions and objects. Checking happens before the program runs, at compile time or in the editor, and then plain JavaScript without types runs in the browser or Node. This catches typos and wrong arguments early and gives autocomplete. Types also work as living documentation for the code.
Follow-up question: What is the
interfacekeyword for?- Course: TypeScript Essentials
- Knowledge base: TypeScript
- Source: developer.mozilla.org
2. Will TypeScript protect you from invalid data that a client sent in a request? Why?
Answer
No. Types exist only during development and compilation, and are erased at runtime. So
JSON.parsereturnsany, and a type annotation on its result is just a promise nobody verifies. Data from outside (requests, files, queues, environment variables) must be checked at runtime, that is, validated, for example with a schema or a DTO with a validator.Follow-up question: Where do you describe such a check in NestJS?
- Course: TypeScript Essentials
- Knowledge base: TypeScript
3. Why does TypeScript complain about an extra field in a fresh object literal, but not when the same object is first put into a variable? Does the type trim the extra field at runtime?
Answer
The excess property check applies only to a fresh literal that is assigned directly to a typed place. For a variable, ordinary structural compatibility applies: if the required fields are present, extra ones are allowed. At runtime the type trims nothing, so a field that is not in the type still goes, for example, into the API's JSON response. To make sure
toResponse(user)cannot returnpasswordHash, you must build the result explicitly from allowed fields instead of relying on the type.Follow-up question: How do you make sure a function physically cannot return
passwordHash, even if the entity has it?- Course: TypeScript Essentials
- Knowledge base: TypeScript
- Source: typescriptlang.org
4. Why can
let role = 'admin'not be passed to a function that accepts'admin' | 'user', whileconst role = 'admin'can? What doesas constadd?Answer
letwidens the literal tostring, because the variable can be reassigned, whileconstkeeps the literal type'admin'.as constmakes an array a readonly tuple of literals and an object one with readonly literal fields. From it(typeof ROLES)[number]gives the union'admin' | 'user'. Node runs.tsdirectly through type stripping and does not check types, so checking is done withtsc --noEmit.Follow-up question: How do you get a union of keys and a union of values from an object
STATUS = { NEW: 'new', PAID: 'paid' } as const, and why is it better thanenum?- Course: TypeScript Essentials
- Knowledge base: TypeScript
- Source: typescriptlang.org
5. A parameter has the type
string | string[] | null. Why doesif (typeof input === 'object')not removenull, and how do you narrow the type correctly?Answer
typeof nullreturns'object', so such a check does not excludenull, and TypeScript rightly saysinputmay benull. At runtime this would give a TypeError. Correct options: an explicitinput !== null,Array.isArray(input)for an array, orinput?.length. In general narrowing must match the real behavior of JavaScript, not intuition.Follow-up question: How does
'amount' in ediffer frome.amount !== undefinedfor narrowing a union, and when doesingive a false result?- Course: TypeScript Essentials
- Knowledge base: TypeScript
- Source: typescriptlang.org
6. How does
Record<Status, string>differ from{ [key: string]: string }? What happens if a new value is added to theStatusunion?Answer
Recordwith a union of keys requires all keys: if a new value is added toStatus, the compiler shows an error in every map where it is missing. This is an exhaustiveness check for maps. An index signature says that any string key exists, and that is untrue: readingloose.refunded.lengthcompiles and then fails at runtime with a TypeError because the value isundefined. For a closed set of keysRecordis more honest.Follow-up question: How do you type a map of allowed order status transitions so that
tsccatches both a forgotten status and a transition to a nonexistent one?- Course: TypeScript Essentials
- Knowledge base: TypeScript
- Source: typescriptlang.org
7. What should the return type of an async function be, and why does
users.map(async (u) => fetchUser(u.id))not give an array of users?Answer
The return type of an async function is always
Promise<T>, for examplePromise<User>, otherwise the compiler reports an error.mapwith an async callback returns an array of promises, not an array of values, so you cannot access fields of the result. The right way is to wait withawait Promise.all(...)before further processing, such asfilter. For a tuple of promisesPromise.allreturns a tuple of values, whileallSettledreturns a tuple of objects with a status.Follow-up question: How does the result type of
Promise.alldiffer fromPromise.allSettledfor the tuple[Promise<User>, Promise<Order[]>]?- Course: TypeScript Essentials
- Knowledge base: TypeScript
- Source: typescriptlang.org
Middle questions
1. How does extending through
interface extendsdiffer from&for types? When do you chooseinterfaceand whentype?Answer
extendschecks compatibility immediately at declaration: if a field conflicts, there is an error in the interface itself.&silently givesneverfor a conflicting field, and the error appears only when you try to create a value. Aninterfacemerges when declared again (declaration merging), whiletypegives Duplicate identifier. A practical rule:interfacefor object shapes and extending external types,typefor union, mapped and conditional types.Follow-up question: How do you add a
userfield toRequestin a Node project through declaration merging, and where do you put the file so thattscpicks it up?- Knowledge base: TypeScript
- Source: typescriptlang.org
2.
JSON.parsereturnsany. How doany,unknownandneverdiffer? What type should a function that reads a payload from a queue return?Answer
anyturns off checking and infects everything it touches.unknownaccepts anything but does not let you do anything without narrowing.neveris a value that does not exist: the bottom type that appears in exhaustiveswitchstatements and in functions that only throw. A payload from a queue is typed asunknownand narrowed through validation, not throughas.Follow-up question: A project has 400 explicit
any. How do you migrate them tounknowngradually without stopping development?- Knowledge base: TypeScript
- Source: typescriptlang.org
3.
refundedwas added to the events union, but the handler was not updated. How do you make the compiler catch this, and why is it better thandefault: return 'unknown'?Answer
For a discriminated union, a
switchon thetypefield narrows the type in each branch. Indefaultonly the unhandled variant remains, and with full coverage that isnever. If you callassertNever(e)there, a new event becomes a compile error (the value is not assignable tonever). A silentdefaulthides the new event instead, and the bug is found in production.Follow-up question: The producer already sends
refunded, but the consumer is deployed with the old union. What happens at runtime and how do you handle it without losing messages?- Knowledge base: TypeScript
- Source: typescriptlang.org
4. A custom type guard
x is Userlets through an object withoutemail, the code compiles without errors, and in production it fails. Who is to blame and how do you fix it?Answer
A type predicate is the function author's promise, the compiler does not check its body. If the guard checks only
id, it lets through an object withoutemail, and thenuser.email.toLowerCase()throws a TypeError. Fix: check every required field or use a schema (zodsafeParse, class-validator). A separate test for the guard catches the case when it became outdated after adding a new required field.Follow-up question: How do you write a test that catches a guard that became outdated after a new required field was added to
User?- Knowledge base: TypeScript
- Source: typescriptlang.org
5. How do you type
pluck(items, key)so that the key is checked and the result has the right type? What areK extends keyof TandT[K]?Answer
The signature is
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][].Tis inferred from the array,Kfrom the key literal, andT[K]is an indexed access type: the type of the field. The constraintK extends keyof Tgives both autocomplete and an error if the field name is misspelled. Forpluck(users, 'age')the result isnumber[].Follow-up question: How do you type
pick(obj, ['id', 'email'])so that the result has the typePick<User, 'id' | 'email'>?- Knowledge base: TypeScript
- Source: typescriptlang.org
6. A function
get<T>(key): Treads fromprocess.envand returns<number>. Everything compiles, but at runtime it is a string. Why does the generic guarantee nothing here, and how is it fixed?Answer
process.envalways holds strings. A generic only in the return type is a hiddenas: the compiler trusts<number>, and there is no runtime check. The right way is to validate and convert environment variables at application start, for example with a schema, and access them through a typed config. ThenPORT=abcbreaks startup immediately, not on the first request.Follow-up question: The app started in staging with
PORT=abcand crashed only on the first request. Where exactly in bootstrap do you put the check?- Knowledge base: TypeScript, Node.js
- Source: docs.nestjs.com
7. Why does
Omit<User, 'pasword'>with a typo compile whilePick<User, 'pasword'>does not? What follows for a response DTO when the fieldpasswordHashis renamed?Answer
The second parameter of
Omitis constrained bykeyof any, notkeyof T, so a typo is not an error.PickrequiresK extends keyof Tand catches it. IfpasswordHashis renamed,Omit<User, 'passwordHash'>silently stops excluding anything, and the hash goes into the response. Fix: your ownStrictOmit<T, K extends keyof T>orPickwith an explicit list of allowed fields (a whitelist instead of a blacklist).Follow-up question: Why did the TypeScript authors deliberately make
Omitnon-strict, and in which generic cases does a strict variant get in the way?- Knowledge base: TypeScript
- Source: typescriptlang.org
8. How do you get the result type of an async service method without duplication? Why are
AwaitedandNonNullableneeded here?Answer
ReturnType<typeof service.find>for an async method givesPromise<User | null>, notUser.Awaitedunwraps thePromise, andNonNullableremovesnullandundefined, soNonNullable<Awaited<ReturnType<...>>>isUser. This way the type comes straight from the service signature and cannot drift from it. For overloaded functionsReturnTypetakes only the last signature.Follow-up question: What does
ReturnTypegive for an overloaded function with three signatures?- Knowledge base: TypeScript
- Source: typescriptlang.org
9. How do you get, using a mapped type with
as, a typeGetters<User>with the keysgetNameandgetAge?Answer
A mapped type with key remapping:
{ [K in keyof T asget${Capitalize<string & K>}]: () => T[K] }. Theaspart transforms the key, andstring & Kdrops keys of typesymbolandnumberso thatCapitalizeworks. ForUser { name: string; age: number }you getgetName: () => stringandgetAge: () => number. This pattern is used for getters, events and filtering fields by type.Follow-up question: How do you get from an entity class, through key remapping, a type with only data fields, without methods?
- Knowledge base: TypeScript
- Source: typescriptlang.org
10. What does
inferdo in a conditional type? How do you get the parameter type and return type of a handler without writing them by hand?Answer
inferintroduces a type variable insideextendsand fills it when the pattern matches. For example,T extends (p: infer P) => infer R ? [P, R] : neverextracts the parameter type and the return type. The built-inParameters,ReturnTypeandAwaitedare made the same way. This lets you derive types from existing functions instead of duplicating them, and keeps them consistent when things change.Follow-up question: How do you get the element type of an array and the value type of a
Map<string, Order>throughinfer?- Knowledge base: TypeScript
- Source: typescriptlang.org
11. How does
privatein TypeScript differ from#fieldin JavaScript? What willJSON.stringifyshow for each?Answer
privateis checked only by the compiler and erased, so at runtime the field is ordinary:JSON.stringify,Object.keysand logs show it.#pinis a truly private JavaScript field, inaccessible from outside at runtime, and serialization does not see it. Consequence: aprivate apiKeyeasily ends up in an API response or a log, so for secrets#fieldis safer. With private fields Proxy and spies can throw a TypeError.Follow-up question: Why can a Proxy on an object with a
#privatefield throw a TypeError when a method is called, and how do you avoid it?- Knowledge base: TypeScript
- Source: typescriptlang.org
12. Why does the variable
eincatch (e)have the typeunknownunderstrict, and how do you correctly get the error message?Answer
Under
strict,useUnknownInCatchVariablesapplies:throwcan throw anything, even a string or a number, sounknownis the honest type. Accessinge.messagewithout a check gives a compile error, and at runtime for a thrown string it would beundefined. The right way:e instanceof Error ? e.message : String(e).catch (e: any)only hides the problem.Follow-up question: How do you type ORM errors (for example, a unique violation) in a service so that you convert them into a domain error without pulling the ORM into the controller?
- Knowledge base: TypeScript
- Source: typescriptlang.org
13. Does
strict: truecatch access to a nonexistent array element or map key? What doesnoUncheckedIndexedAccesschange?Answer
No. With
strict,arr[0]has the element type andbyId['u1']the value type, although both can beundefinedat runtime, so there are zero compile errors. ThenoUncheckedIndexedAccessflag adds| undefinedto such reads, and then the compiler forces checks. It is not part ofstrictand is enabled separately. The cost: noise in index loops, which is cured withfor...of,at()with a check, or an explicit branch.Follow-up question: How do you enable
noUncheckedIndexedAccessgradually in a large monorepo without stopping development?- Knowledge base: TypeScript
- Source: typescriptlang.org
14.
tsconfighaspaths: { '@app/*': ['src/*'] },tscpasses, butnode dist/main.jsfails in Docker. What is the error and why?Answer
The error is
Cannot find module '@app/...'(MODULE_NOT_FOUND).pathsis only a hint for resolving types:tscdoes not rewrite the specifier in the generated JS. Locally it often works throughts-nodewithtsconfig-pathsor a dev build server, while plaintscin Docker does not. Fix: subpath imports (#app/*) inpackage.json,tsconfig-paths/register,tsc-aliasor a bundler. In CI it is worth checking that the builtdistreally starts.Follow-up question: How do you configure Jest so that tests see the same aliases?
- Knowledge base: TypeScript, Node.js
- Source: typescriptlang.org
15. How does a numeric
enumdiffer from a string one at runtime? Why doesenumbreak when running.tsthrough type stripping?Answer
A numeric
enumcreates a reverse mapping:Object.keys(Status)gives['0', '1', 'Active', 'Blocked'], andStatus[0]returns'Active'. A stringenumhas no reverse mapping. Anenumgenerates real runtime code, so with type stripping Node cannot simply erase it and reports an unsupported syntax error. Alternative: a union of literals with anas constobject. Also a numericenumis dangerous if numbers are stored in the DB and a new value is inserted in the middle.Follow-up question: A status is stored in the DB as a number from a numeric
enum. Someone inserted a new status in the middle. What happens to the existing data?- Knowledge base: TypeScript
- Source: typescriptlang.org
16. Why does
Object.keys(user)returnstring[]and not(keyof User)[]? Why is the fix throughas (keyof User)[]dangerous?Answer
TypeScript types structurally, so at runtime an object may have extra keys that the type does not describe.
string[]is the honest answer. The castas (keyof User)[]compiles but may let through, for example,loginCount, and later indexed access will giveundefined. Safer: an explicit list of keys,Object.entrieswith a check, or aMap.Follow-up question: How do you write a typed function
entries<T>(obj: T), and why will its type also be partly a lie?- Knowledge base: TypeScript
- Source: typescriptlang.org
17. The code compiles under
strict, review passed, and in prod it fails for some users. Which two places withasand!most often 'lie', and what do you do about it?Answer
as Userdoes not check the data, it only turns off type checking: if the real object is different, the error comes at runtime.!removesundefinedfrom the type with no runtime check, souser.profile!.emailfails if there is no profile. Fix: validateunknownwith a schema, use?.and an explicit branch for the missing value. A useful lint rule:no-non-null-assertionand limitingasto system boundaries.Follow-up question: How do
as constandsatisfiesdiffer from a plainasin terms of type safety?- Knowledge base: TypeScript
- Source: typescriptlang.org
18. An import endpoint responds 200 before the records are saved, and save errors are not visible anywhere. Inside is
items.forEach(async (i) => await save(i)). Why did TypeScript not catch this?Answer
forEachdoes not wait for the promises from an async callback and returnsundefined, so the code moves on and the response is sent earlier. TypeScript allows a callback that returns aPromisewherevoidis expected. A rejected promise is handled by nobody: it is anunhandledRejection, which in Node 15+ can crash the process. Fix:for...ofwithawaitorPromise.all(with limited concurrency), plus the lint rulesno-floating-promisesandno-misused-promises.Follow-up question: For an import of 50 thousand rows: how do you limit concurrency to 10 and not keep the HTTP request open for minutes?
- Knowledge base: TypeScript, Node.js
- Source: nodejs.org
Senior questions
1. Why does an assertion function
asserts v is ...work when declared withfunction, but not when the same signature is in aconstwith an arrow?Answer
For narrowing through
asserts, TypeScript requires the call name to have an explicit type annotation. Afunction declarationhas an explicit signature, whileconst x = (v) => ...gets an inferred type, and the compiler reports that an explicit annotation is needed. So the narrowing does not apply and later field access is still considered unsafe. Fix:functionor an explicit annotationconst assertOk: <T>(v: T) => asserts v is NonNullable<T>.Follow-up question: When would you choose an assertion function that throws in a service, and when would you return
null?- Knowledge base: TypeScript
- Source: typescriptlang.org
2. A value has already been checked in
if, but inside aforEachcallback the compiler again considers it possiblyundefined. Why, and what changed in TypeScript 5.4?Answer
Narrowing of a property (
ctx.user) does not survive a callback, because the compiler cannot guarantee the property will not be changed between the check and the call. Forletthe problem is that the variable may be reassigned after the closure is created. Since TypeScript 5.4, narrowing is preserved in closures if there are no assignments to the variable after it. For properties the reliable fix isconst user = ctx.userbefore theif.Follow-up question: Is narrowing preserved inside an async callback after
await?- Knowledge base: TypeScript
- Source: typescriptlang.org
3. How does TypeScript infer
Tfrom several arguments? Why doesmerge(a, b)compile for two objects butmerge(1, '1')does not? How do you requirebto be the same type asa?Answer
The compiler collects candidates for
Tfrom the arguments. For objects it can combine the shapes into a union, so the call compiles. For primitivesTis fixed by the first candidate (1), and the second argument'1'does not fit.NoInfer<T>(TypeScript 5.4) removes a position from the inference candidates, sob: NoInfer<T>must match theTalready inferred froma.Follow-up question: How does a
consttype parameter (function f<const T>(x: T)) work and where is it useful?- Knowledge base: TypeScript
- Source: typescriptlang.org
4. A PATCH accepts
Partial<User>and does{ ...user, ...patch }. The types pass, but the user'snamedisappears. Why, and whichtsconfigflag catches it?Answer
Partialmakes a field optional, and an optional field without an extra flag allows an explicitundefinedvalue. Spread withundefinedoverwrites the existing value, while the result type is stillUser. TheexactOptionalPropertyTypesflag forbids an explicitundefinedfor optional fields and gives an error at the call. In PATCH you should stripundefinedbefore merging or use a whitelist of fields and explicit mapping.Follow-up question: How do you distinguish three states of a field in PATCH: not provided, provided as
null, provided with a value?- Knowledge base: TypeScript
- Source: typescriptlang.org
5. What is a distributive conditional type? Why does
ToArray<string | number>givestring[] | number[], while a version with the[T]wrapper gives(string | number)[]?Answer
A conditional type with a 'naked' parameter
Tis distributive over a union: it is applied to each member separately, and the results are combined. SoT extends unknown ? T[] : neverforstring | numbergivesstring[] | number[]. The wrapper[T] extends [unknown]turns off distribution and treats the union as a whole, hence(string | number)[].ExcludeandExtractare built on distribution.Follow-up question: Write a type that extracts from a union of events only those that have an
amountfield, and explain where distribution works.- Knowledge base: TypeScript
- Source: typescriptlang.org
6. How do you use template literal types to type route parameters like
/orgs/:orgId/users/:userId? What is the cost of such recursion?Answer
A recursive conditional type splits the string into segments through
inferand collects the parameter names into a union:'orgId' | 'userId'. Then the function requires an object with these keys, and omittinguserIdgives an error. But these are only types: at runtimenodechecks nothing. The cost: deep recursion can hit a limit (TS2589) and slow down the IDE, so such types are kept simple.Follow-up question: How do you extend the type so that the parameter
:id?becomes an optional key in the resulting object?- Knowledge base: TypeScript
- Source: typescriptlang.org
7.
package.jsonhas"type": "module", and intsconfigbothmoduleandmoduleResolutionarenodenext. Why doesimport './users'give an error, and why do you write./users.jsin a.tsfile?Answer
nodenextmodels the real Node resolver: in ESM a relative import needs an extension, sotscgives an error with the hint./users.js. Node itself does not guess extensions and would throwERR_MODULE_NOT_FOUND. You write.jsin.tsbecause the import points to the file after compilation. Thebundlermode allows imports without extensions, but only when the code goes through a bundler. For running through type stripping you write.tsin imports and enablerewriteRelativeImportExtensions.Follow-up question: A CommonJS project has a dependency that became ESM only. What options are there?
- Knowledge base: TypeScript, Node.js
- Source: typescriptlang.org
8. What are branded types and why are they used for identifiers like
UserIdandOrderId? Does the brand exist at runtime?Answer
A branded type is a primitive with a 'tag':
string & { __brand: 'OrderId' }. This way aUserIdcannot be passed where anOrderIdis expected, and a barestringdoes not fit either, which catches mixed up arguments in an order service. The brand exists only in the type: at runtime it is an ordinary string. The tag is applied in one place, after a check (parsing), not throughasin every call.Follow-up question: How do you carry a branded id from a route parameter through a pipe into a service and back into a response without
asin controllers?- Knowledge base: TypeScript
- Source: typescriptlang.org
9. Why does TypeScript under
strictFunctionTypescatch an unsafe assignment of a handler declared as a function property, but let the same pass for a method?Answer
A function property (
handler: (e: Event) => void) is checked contravariantly on parameters, so a narrower handler cannot be assigned. Method parameters (handler(e: Event): void) stay bivariant even withstrictFunctionTypes, so the unsafe assignment compiles, and at runtime the handler receives an event without the needed field and fails. Fix: declare callbacks as function properties and register narrow handlers by event type. It is a similar story withArray<Dog>being assignable toArray<Animal>.Follow-up question: Why is
Array<Dog>assignable toArray<Animal>, and how does this break code that doespushinto such an array?- Knowledge base: TypeScript
- Source: typescriptlang.org
10. In Nest with
emitDecoratorMetadata, a dependency imported throughimport typebreaks DI. What exactly doestscgenerate indesign:paramtypesfor an interface and forimport type?Answer
For an interface the compiler writes
Object, because interfaces are erased. For a class imported throughimport typethe reference to the class disappears, and the metadata will holdFunctionorObject. The DI container readsdesign:paramtypesthroughreflect-metadataand looks for a provider by that value, soObjectandFunctionare not tokens, and startup fails with an unresolved dependencies error. Fix: a regularimportof the class,@Injectwith an explicit token for an interface,forwardReffor a cycle.Follow-up question: How do you configure the
consistent-type-importsrule so that it does not break DI?- Knowledge base: TypeScript, NestJS
- Source: typescriptlang.org
11. A payment service has expected failures (card declined, insufficient funds) and unexpected ones (network, a bug). How do you type them: exceptions or Result? How do they reach the HTTP response?
Answer
I separate expected business failures from unexpected faults. Expected ones I return as a
Result, a discriminated union: the compiler forces handling of every variant. Unexpected ones stay as exceptions: a global exception filter returns 500 and logs. I map a domain error to an HTTP status at the boundary (a controller or a filter), and the domain layer does not throw HTTP exceptions. Expected failures should also be counted in metrics separately from 5xx, so that they do not wake the on-call engineer.Follow-up question: How do you log and count expected failures in metrics so that they do not mix with 5xx?
- Knowledge base: TypeScript, NestJS
- Source: docs.nestjs.com
12. A team wants a base generic repository
Repository<T extends Entity>for all services. What is good about it in terms of types, what doestsccatch, and where do you stop generalizing?Answer
The constraint
T extends Entityguarantees the presence ofid, and a default type parameter allowsRepositorywithout an argument. A methodfindBy<K extends keyof T>(key: K, value: T[K])catches invalid keys and values, and a type withoutidfails the constraint. The limit of generalization: specific queries live in concrete repositories, and the base class is not bloated. Also, do not duplicate the ORM's ownRepository<T>and do not leak ORM types into the domain layer.Follow-up question: How do you register several instances of a generic repository for different entities in Nest and inject them without token collisions?
- Knowledge base: TypeScript, NestJS
- Source: typescriptlang.org
Train these questions
Study the cards, answer by text or voice and get an AI grade. Needs a signed in account.
Answer out loud and get graded
Here you get the questions with model answers. In the ShortLesson app you answer yourself, by text or voice, and AI grades the answer against the rubric and asks a follow-up, like a real interviewer.
The app is coming to the stores soon.
Other topics
- JavaScript interview questions and answers
- React interview questions and answers
- Frontend performance interview questions and answers
- Node.js interview questions and answers
- HTTP interview questions and answers
- SQL interview questions and answers
- Databases interview questions and answers
- Git interview questions and answers
- Algorithms interview questions and answers
- System design interview questions and answers
- DevOps interview questions and answers
- Web security interview questions and answers
- Testing interview questions and answers