TypeScript Interview Questions and Answers: 60 Questions from Easy to Hard
This guide is for freshers and experienced developers in India preparing for front-end, full-stack and Node.js interviews where TypeScript is on the job description. It has 60 questions in three levels. Easy covers what every fresher is asked: basic types, type aliases and interfaces, unions, any versus unknown, functions, enums and tsconfig. Medium covers daily project work: narrowing, generics with constraints, utility types, typing React components and API responses, and strict mode errors. Hard covers senior topics: conditional and mapped types, variance, type erasure, declaration files, large builds and migrating a JavaScript codebase. Answers assume TypeScript 5.x with strict mode on. Answer each question aloud first, then compare, and finish with the mock test.
- 60 questions, 3 levels
- Updated
- CoreHRx Jobs
- 1,052 open TypeScript jobs
What TypeScript interviews cover
- Type system basics: primitive and object types, type aliases versus interfaces, unions, intersections and literal types.
- Everyday safety: narrowing with typeof, instanceof and discriminated unions, strict null checks, and any versus unknown versus never.
- Reuse: generics with constraints and the built-in utility types, and for senior roles mapped and conditional types.
- Project work: typing React props, state and events, typing API responses, writing declaration files and reading a tsconfig.
- For experienced roles: type erasure and runtime validation, build speed in large codebases, and how you would migrate a JavaScript project.
How to prepare (freshers and experienced)
- Freshers: get fluent in JavaScript first, because many TypeScript questions turn into JavaScript questions about this, closures, promises and equality.
- Write small programs with strict set to true and read every compiler error to the end; most interview questions are really about why an error appears.
- Practise describing a type in plain words, such as a function that takes a list of T and returns a T or undefined, because interviewers listen for that clarity.
- Experienced developers: prepare one real story, such as removing any from a module, a type that caught a production bug, or a migration you led.
- Revise recent TypeScript 5.x features such as satisfies, const type parameters, NoInfer and inferred type predicates, and know which version added each.
Common mistakes and interview-day tips
- Do not say TypeScript checks data at runtime. Types are erased, so data from an API or a form still needs validation.
- Do not use any or an as assertion just to make an error go away; explain how you would narrow the type instead.
- Name the setting when behaviour depends on it, for example: with strictNullChecks on, find returns the element type or undefined.
- In live coding, write the types of the inputs and outputs first and then the function body; it shows design thinking and catches mistakes early.
- If you are unsure about an advanced feature, say how you would check it, for example with a small test in the TypeScript Playground, instead of guessing.
TypeScript interview questions: Easy questions (freshers)
20 questions1. What is TypeScript, and how is it related to JavaScript?
TypeScript is JavaScript with a static type system on top. Most JavaScript is already valid TypeScript syntax, and the compiler, tsc, checks your types and then emits plain JavaScript with the types removed, so the browser or Node.js never sees them. The benefit is catching mistakes such as a misspelt property or a missing argument while you type, plus better autocomplete and safer refactoring. A common misunderstanding is that TypeScript checks data while the program runs; it does not, it only checks the code before it runs.
2. What are the basic types in TypeScript?
The primitive types are string, number, boolean, bigint, symbol, null and undefined. Arrays are written as string[], and a tuple such as [string, number] is an array with a fixed length and a type for each position, for example a city name and its pincode. On top of these come object types, unions and literal types, and the special types any, unknown, void and never. number covers both integers and decimals, because JavaScript has one number type. A common mistake is writing String or Number with a capital letter; those are wrapper object types, so use the lowercase names.
3. What is type inference, and when should you still write a type annotation?
Type inference means the compiler works out a type from the value. let count = 5 makes count a number without any annotation, so assigning the string five to it later is a compile error. With const the type is the literal itself, because a const can never change. Let inference handle local variables and obvious return values. Write annotations on function parameters, which cannot be inferred, on exported functions so the public API stays stable, and on empty arrays or objects, whose type the compiler cannot guess from the first line.
4. What is the difference between a type alias and an interface?
Both can describe the shape of an object, and for plain objects they are nearly interchangeable. An interface describes object shapes only, including callable ones, can be extended with extends, and can be reopened later so that two declarations with the same name merge. A type alias can name any type: unions, tuples, primitives, mapped and conditional types. So a Status union like the one here must be a type. Many teams use interface for object shapes and public APIs and type for everything else; being consistent matters more than the choice.
interface User {
id: number;
name: string;
}
type Status = "active" | "blocked";
type Point = { x: number; y: number };5. How do optional and readonly properties work?
A question mark after a property name, as in phone?: string, makes the property optional, and with strict mode on its type becomes string | undefined, so you must check it before calling string methods on it. The readonly modifier, as in readonly id: number, stops reassignment of that property through this type. Both are compile-time checks only: the emitted JavaScript object can still be changed. readonly is also shallow, so a readonly array property can still be pushed to unless its type is a ReadonlyArray. Use Object.freeze when you need protection at runtime.
6. What is a union type, and how do you work with one safely?
A union type such as string | number means the value can be any one of the listed types. You may only use members that exist on every part of the union, so calling toUpperCase on id directly is an error. To use type-specific methods you narrow the union first: inside the typeof check, TypeScript knows id is a number, and after that branch returns, it knows the rest must be a string. Unions are the normal way to model values that really have several forms, such as an employee ID that comes from two different systems.
function formatId(id: string | number): string {
if (typeof id === "number") {
return id.toFixed(0).padStart(6, "0");
}
return id.toUpperCase();
}
console.log(formatId(42)); // 000042
console.log(formatId("emp7")); // EMP77. What are literal types, and why are they useful?
A literal type is one exact value used as a type, such as "UPI" or 404. Alone it is rarely useful, but a union of literals, for example type PaymentMode = "UPI" | "card" | "netbanking", gives a closed set of allowed values and is a lighter alternative to an enum. The editor suggests the valid strings, a typo such as upi in lowercase is caught at compile time, and the emitted JavaScript is just the plain string with no extra runtime object. Literal unions are also the base of discriminated unions, where a shared literal field tells the compiler which variant an object is.
8. What is the difference between any and unknown?
Both accept any value, but any also switches off type checking for that value, so every property access and call compiles and mistakes only show up at runtime, as with a.foo.bar() here. unknown is the safe version: you can store anything in it, but you cannot use it until you narrow it with typeof, instanceof, in or a type guard. Use unknown for data you do not yet trust, such as parsed JSON or a caught error, and keep any as a rare escape hatch. A real trap is that any spreads: values derived from an any are any too.
let a: any = JSON.parse('{"x": 1}');
a.foo.bar(); // compiles, crashes at runtime
let u: unknown = JSON.parse('{"x": 1}');
u.foo; // Error: u is of type unknown
if (typeof u === "object" && u !== null && "x" in u) {
console.log(u.x); // allowed after the checks
}9. What do void and never mean as return types?
void means a function returns no useful value: callers should not use its result, and at runtime it returns undefined, like a function that only writes a log line. never means the function never returns at all, because it always throws or loops forever, so code after a call to it is unreachable. The compiler uses this: after a call to a function returning never, it treats the rest of the branch as dead code, and never is also what exhaustive switch checks rely on. A common confusion is thinking never means returns nothing; that is void.
10. How do you type function parameters, optional parameters and default values?
Each parameter gets a type after a colon, and the return type goes after the parameter list. A question mark makes a parameter optional, so inside the function its type is string | undefined and you must handle the missing case, as the prefix line does. A default value also makes a parameter optional, and its type is inferred from the default, so city is a string. Optional parameters must come after required ones. TypeScript also checks the number of arguments, so calling greet() with no name is a compile error, unlike in plain JavaScript.
function greet(name: string, title?: string, city = "Delhi"): string {
const prefix = title ? title + " " : "";
return `Hello ${prefix}${name} from ${city}`;
}
console.log(greet("Meena"));
console.log(greet("Meena", "Dr.", "Kochi"));
// Hello Meena from Delhi
// Hello Dr. Meena from Kochi11. What is an enum, and what does it turn into at runtime?
An enum is a named set of constants. A numeric enum starts at 0 and counts up unless you assign values, so Role.Manager is 1. Unlike most TypeScript features, an enum is not only a type: the compiler emits a real JavaScript object, and for numeric enums that object also maps numbers back to names, which is why Role[2] gives Admin. String enums, such as Admin = "ADMIN", have no reverse mapping but read better in logs and API payloads. Many teams now prefer a union of string literals, which adds no runtime code at all.
enum Role {
Employee,
Manager,
Admin,
}
console.log(Role.Manager); // 1
console.log(Role[2]); // Admin12. What is tsconfig.json, and which settings should a fresher know?
tsconfig.json marks the root of a TypeScript project and holds its compiler options, so running tsc with no arguments uses it, and editors read the same file. The settings to know first: target is the JavaScript version to emit, such as ES2022; module is the module system of the output; strict turns on the full family of strict checks; outDir is where compiled files go; rootDir is where the sources live; and include lists the source files or folders. Start new projects with strict set to true, because switching it on later in a big codebase is much harder.
13. What does strict mode do, and why do most teams turn it on?
Setting strict to true in tsconfig enables a family of stricter checks at once. The ones you notice most are strictNullChecks, which keeps null and undefined out of other types unless you add them, and noImplicitAny, which reports parameters and variables that would silently become any. Others check function parameters more strictly, require class properties to be initialised and make caught errors unknown. Teams use it because these flags catch common real bugs, such as reading a property of undefined. New TypeScript versions can add checks under strict, so upgrades may show new errors.
14. What does type erasure mean in TypeScript?
Type erasure means the compiler removes all type information when it emits JavaScript. Interfaces, type aliases, annotations and generic parameters leave no trace in the output, so an Order interface simply does not exist when the code runs. That has practical results: you cannot use instanceof with an interface, a generic function cannot ask at runtime what its type parameter is, and annotating API data with a type does not validate it. Only a few features, such as enums, namespaces with values and class parameter properties, produce runtime code. For runtime checks, write them yourself.
15. How do access modifiers and parameter properties work in TypeScript classes?
public members can be used anywhere, private ones only inside the class, and protected ones inside the class and its subclasses. Writing a modifier on a constructor parameter, as here, is a shortcut that declares the property and assigns it in one step. The key point is that private is a compile-time check only: balance is still an ordinary property at runtime, and any JavaScript code can read it. If you need real runtime privacy, use JavaScript private fields written with a hash sign, such as #balance, which TypeScript also supports and checks.
class Account {
constructor(
public readonly owner: string,
private balance: number,
) {}
deposit(amount: number): number {
this.balance += amount;
return this.balance;
}
}
const acc = new Account("Kiran", 1000);
console.log(acc.deposit(500)); // 1500
// acc.balance; // Error: balance is private16. What is a generic, and why would you use one?
A generic is a type parameter, written in angle brackets, that lets one function, class or type work with many types while keeping the link between input and output. Here T is inferred from the argument, so passing numbers gives back number | undefined and passing strings gives back string | undefined. Without generics you would write one function per type or use any, which loses all checking on the result. Use meaningful names when there are several type parameters, and skip a type parameter that appears only once in a signature, since it adds nothing.
function firstItem<T>(items: T[]): T | undefined {
return items[0];
}
const n = firstItem([10, 20, 30]); // number | undefined
const s = firstItem(["Goa", "Agra"]); // string | undefined
console.log(n, s); // 10 Goa17. What is an intersection type?
An intersection, written with an ampersand, combines several types into one that has all of their members, so an Employee here must have name, email and empCode. It is the type-level way to mix in extra fields, similar to extending an interface. Watch out when the same property appears with conflicting types: intersecting an object whose id is a string with one whose id is a number makes id never, and no value can satisfy it. A common mix-up is thinking A & B means fewer properties because it sounds like a set intersection; for objects it means more.
type Person = { name: string };
type Contact = { email: string };
type Employee = Person & Contact & { empCode: string };
const e: Employee = {
name: "Farhan",
email: "farhan@example.com",
empCode: "BLR-204",
};18. What are type assertions and the non-null assertion operator, and when are they risky?
A type assertion such as el as HTMLInputElement tells the compiler to treat a value as a more specific type; it does not convert or check anything at runtime. For example, getElementById returns HTMLElement | null, and an assertion lets you read .value as if it were an input. An exclamation mark after an expression, the non-null assertion, removes null and undefined from its type. Both are promises you make to the compiler, so if you are wrong the code crashes at runtime with no warning. Prefer a real check, such as an early return when the value is missing.
19. How do you type an object whose keys are not known in advance?
Use an index signature, written as [city: string]: number inside an object type, or the built-in Record utility with string keys and number values, which means the same thing: any string key maps to a number, as in stock counts per city. If the keys are a fixed set, use a union of literals as the key type instead, so a missing or misspelt key is caught. Remember that with a plain string index signature TypeScript assumes every key exists, so reading a city that is not there is typed as number although it is undefined; the noUncheckedIndexedAccess option adds undefined to such reads.
20. How do you type the props of a React function component?
Declare a type or interface for the props and annotate the destructured parameter with it. The compiler then checks every place the component is used: a missing label, a wrong variant string or an onClick with the wrong signature is an error in the parent component, not a runtime bug. Optional props get a question mark and usually a default value in the destructuring, as variant does here. You do not need to write the return type; it is inferred as a JSX element. Avoid typing props as any, because you lose the main benefit of TypeScript in React: checked component usage.
type ButtonProps = {
label: string;
variant?: "primary" | "secondary";
onClick: () => void;
};
function Button({ label, variant = "primary", onClick }: ButtonProps) {
return (
<button className={variant} onClick={onClick}>
{label}
</button>
);
}Ready to check yourself? Take the free TypeScript mock test
TypeScript interview questions: Medium questions (1-3 years)
20 questions21. In a real project, how do you decide between type and interface?
Use a type alias whenever you need something an interface cannot express: unions of object shapes, tuples, and mapped or conditional types. For plain object shapes either works, so decide on practical grounds. Interfaces support declaration merging, which you want when augmenting a library type and do not want by accident, because two interfaces with the same name in one scope silently combine. extends on an interface also reports conflicting properties as an error, while an intersection can quietly turn a property into never. Pick a rule for the codebase and enforce it with a lint rule.
22. What narrowing techniques does TypeScript understand?
The compiler follows your control flow and narrows a union in each branch. It understands typeof checks for primitives, instanceof for classes, Array.isArray, equality checks against null, undefined or literal values, truthiness checks, the in operator for property names, and user-defined type guards. After an early return, the rest of the function sees only the remaining types, which is why x.status needs no extra check at the end. A common mistake is using typeof x === "object" to rule out null; typeof null is also "object", so add an explicit null check.
class ApiError extends Error {
constructor(public status: number) {
super("API error");
}
}
function describe(x: string | number[] | ApiError | null) {
if (x === null) return "nothing";
if (typeof x === "string") return "text of " + x.length;
if (Array.isArray(x)) return "list of " + x.length;
return "error " + x.status;
}
console.log(describe("hello")); // text of 5
console.log(describe([1, 2])); // list of 2
console.log(describe(new ApiError(503))); // error 50323. What is a discriminated union, and how do you make a switch exhaustive?
A discriminated union is a union of object types that share one literal field, here kind. Checking that field narrows the whole object, so inside case "upi" you can use p.vpa safely. The default branch assigns p to a variable of type never, which only compiles when every variant has been handled. When someone later adds a wallet variant, that line becomes a compile error and points at every switch that needs updating. This is the standard way to model payment types, API states or Redux actions, and it replaces fragile objects with many optional fields.
type Payment =
| { kind: "upi"; vpa: string }
| { kind: "card"; last4: string }
| { kind: "cod" };
function label(p: Payment): string {
switch (p.kind) {
case "upi": return "UPI " + p.vpa;
case "card": return "Card ending " + p.last4;
case "cod": return "Cash on delivery";
default: {
const unreachable: never = p;
return unreachable;
}
}
}
console.log(label({ kind: "card", last4: "4321" }));
// Card ending 432124. How do you write a user-defined type guard and an assertion function?
A type guard is a function that returns a boolean and whose return type is written as u is Admin; when it returns true, the caller's variable is narrowed to Admin. An assertion function uses asserts v is T and returns nothing; if it returns at all, the compiler treats the condition as proven for the code after the call, and if the check fails it throws. Both let you reuse a check across files. The risk is that the compiler trusts your function completely: if the body checks the wrong thing, the types lie. Keep guards small and unit test them.
type Admin = { role: "admin"; permissions: string[] };
type Staff = { role: "staff"; team: string };
function isAdmin(u: Admin | Staff): u is Admin {
return u.role === "admin";
}
function assertDefined<T>(
v: T,
msg: string,
): asserts v is NonNullable<T> {
if (v === null || v === undefined) throw new Error(msg);
}25. How do generic constraints with extends and keyof work?
A constraint such as K extends keyof T limits what a type parameter may be; here K must be one of the property names of T. That lets the function return T[K][], an indexed access type, so plucking city gives string[] and plucking exp gives number[]. Without the constraint, item[key] would not compile, because the compiler could not prove the key exists, and a wrong key such as salary is now caught at the call site. Constraints also work with shapes, such as T extends an object with an id, which lets the body read item.id while still returning the caller's full type.
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
const staff = [
{ name: "Asha", city: "Pune", exp: 3 },
{ name: "Vikram", city: "Jaipur", exp: 6 },
];
console.log(pluck(staff, "city")); // [ 'Pune', 'Jaipur' ]
// pluck(staff, "salary"); // Error: not a key of the items26. Which built-in utility types do you use most, and what does each do?
Partial makes every property optional, Required makes them all required, Readonly makes them read-only, Pick keeps the listed keys, Omit removes them, and Record builds an object type from a key type and a value type. They stop you from copying an interface and letting the copies drift apart. A typical PATCH payload is Omit of Employee without id, wrapped in Partial: every field is optional and the id cannot be changed. Note that Omit does not check that the keys you remove exist, so a typo in Omit silently removes nothing, while Pick does check its keys.
interface Employee {
id: number;
name: string;
email: string;
managerId?: number;
}
type EmployeeUpdate = Partial<Omit<Employee, "id">>;
type EmployeeCard = Pick<Employee, "name" | "email">;
type FrozenEmployee = Readonly<Employee>;
type ByCity = Record<string, Employee[]>;27. How do ReturnType, Parameters, Awaited and NonNullable help you?
These utilities read types off existing code so you do not write them twice. Parameters gives the tuple of a function's parameter types and ReturnType gives its return type; both take a function type, so you write typeof myFunction, not the function value itself. An async function returns a Promise, so Awaited unwraps it, including nested promises, to get the resolved object. NonNullable removes null and undefined from a union. Deriving types like this keeps them in sync: when the function changes, every derived type follows. Give the result a name so readers can see the shape.
28. How do you type an API response from fetch safely?
response.json() is typed as a Promise of any, so writing const job: Job = await res.json() compiles but proves nothing; the server can send anything. Treat the body as unknown and check it at the boundary, with a type guard like isJob or a schema validation library that can also give you the type from the schema. Also check res.ok, because fetch does not reject on a 404 or 500. Once the data is validated, the rest of the app can trust the Job type. A common mistake is a generic get function that just returns whatever type you ask for, which is a hidden assertion.
type Job = { id: number; title: string; city: string };
function isJob(x: unknown): x is Job {
return typeof x === "object" && x !== null
&& typeof (x as Job).id === "number"
&& typeof (x as Job).title === "string"
&& typeof (x as Job).city === "string";
}
async function getJob(id: number): Promise<Job> {
const res = await fetch(`/api/jobs/${id}`);
if (!res.ok) throw new Error("HTTP " + res.status);
const data: unknown = await res.json();
if (!isJob(data)) throw new Error("Unexpected response");
return data;
}29. How would you model loading, success and error states for an API call?
Use a generic discriminated union with one variant per state: idle, loading, success carrying data, and error carrying a message. The flag-based alternative, an object with isLoading, an optional data field and an optional error field, allows impossible combinations such as loading with an error and data at once, and forces null checks everywhere. With the union, each state carries only the fields that make sense, and checking the status field narrows the object, so data exists only in the success branch. A switch over status is then exhaustive, and adding a state shows every place to update.
30. How do you type state, events and children in React components?
useState infers its type from the initial value, so useState("") is already a string. Pass the type explicitly when the initial value does not show the full type, as with a value that starts as null or an empty array of objects. Event handlers use React's event types, such as ChangeEvent for an input, with HTMLInputElement as its type argument, or FormEvent for a form submit. For children, ReactNode is the usual type because it accepts text, elements, arrays and null. A handler written inline in the JSX gets its event type inferred, so it needs no annotation.
import { useState, type ChangeEvent, type ReactNode } from "react";
type CardProps = { title: string; children: ReactNode };
function Card({ title, children }: CardProps) {
return <section><h2>{title}</h2>{children}</section>;
}
function PincodeInput() {
const [pin, setPin] = useState("");
const [error, setError] = useState<string | null>(null);
const onChange = (e: ChangeEvent<HTMLInputElement>) =>
setPin(e.target.value);
return <input value={pin} onChange={onChange} />;
}31. Should you type components with React.FC? What changed in the React 18 types?
React.FC works, but many teams now prefer a plain function with typed props. The historical problem was that FC used to add an implicit children prop, so a component that ignored children still accepted them silently. The @types/react 18 release removed that implicit children, so with either style you now declare children yourself when a component takes them. A plain function is also easier to make generic, because FC gives you no place for a type parameter, and it reads like every other function. Whichever you choose, keep it consistent across the codebase.
32. How do keyof and typeof work as type operators?
In a type position, typeof takes a value and gives its type, and keyof takes an object type and gives the union of its keys. Combined as keyof typeof PLAN_PRICES, they turn a runtime object into a union of allowed plan names, so the list of plans lives in one place and a plan added to the object is accepted by price automatically. Do not confuse the type operator with JavaScript's runtime typeof, which returns a string such as "object". Also note that keyof on a type with a string index signature gives string | number, not string.
const PLAN_PRICES = {
basic: 199,
pro: 499,
enterprise: 1999,
};
type Plan = keyof typeof PLAN_PRICES;
// "basic" | "pro" | "enterprise"
function price(plan: Plan): number {
return PLAN_PRICES[plan];
}
console.log(price("pro")); // 49933. What does as const do, and when is it useful?
as const tells the compiler to infer the narrowest type: strings and numbers keep their literal types, arrays become readonly tuples and object properties become readonly. Without it, ROLES would be string[] and Role would just be string. An as const array plus (typeof ROLES)[number] gives a list you can loop over at runtime and a matching union type, from one source. It changes types only; the array is not frozen at runtime. One gotcha: includes on a readonly tuple of literals rejects a plain string argument, which is why isRole widens the array before calling it.
const ROLES = ["admin", "hr", "employee"] as const;
type Role = (typeof ROLES)[number];
// "admin" | "hr" | "employee"
const config = { env: "prod", retries: 3 } as const;
// { readonly env: "prod"; readonly retries: 3 }
function isRole(x: string): x is Role {
return (ROLES as readonly string[]).includes(x);
}
console.log(isRole("hr"), isRole("ceo")); // true false34. What is the satisfies operator, and how is it different from a type annotation?
satisfies, added in TypeScript 4.9, checks that a value matches a type but keeps the value's own, more specific inferred type. With an annotation of a wide type, such as a Record of string to Route, the variable takes that type, so the compiler forgets which keys exist and allows loose.reports. With satisfies you still get an error if a route is missing auth or has a misspelt field, yet routes keeps its exact keys for autocomplete and checking. Use it for config objects and lookup tables where you want validation and precise inference together.
type Route = { path: string; auth: boolean };
const routes = {
home: { path: "/", auth: false },
payroll: { path: "/payroll", auth: true },
} satisfies Record<string, Route>;
routes.payroll.path; // fine: the key is known
// routes.reports; // Error: no such key
const loose: Record<string, Route> = routes;
loose.reports; // compiles, undefined at runtime35. How should you handle errors in a catch block with strict mode on?
Since TypeScript 4.4, the useUnknownInCatchVariables flag, which strict includes, types the catch variable as unknown instead of any. That is honest, because JavaScript lets you throw anything: a string, a number or an object with no message. So narrow before use, usually with instanceof Error to read err.message, and handle the other case with String(err). Reading err.message directly is now a compile error, which catches a real bug. Do not silence it by writing catch (err: any); the only annotations allowed on a catch variable are any and unknown, and any throws the safety away.
36. What are function overloads, and when would you use them?
Overloads give one function several call signatures, so the return type can depend on the argument type. You write the overload signatures first, then one implementation signature wide enough for all of them; callers see only the overloads. Here a string gives a number and an array gives a number[], which a single union signature cannot express. The body is checked only against the wide implementation signature, so nothing proves a string input really returns a number; test it. A union parameter or a generic is often simpler; use overloads only when the input decides the output type.
function parseAmount(input: string): number;
function parseAmount(input: string[]): number[];
function parseAmount(input: string | string[]): number | number[] {
if (Array.isArray(input)) return input.map(s => Number(s));
return Number(input);
}
const one = parseAmount("250"); // number
const many = parseAmount(["10", "20"]); // number[]
console.log(one, many); // 250 [ 10, 20 ]37. Why does Array.find give a possibly undefined error, and how do you fix it properly?
With strictNullChecks on, find returns the element type or undefined, because nothing may match. The error describes a real runtime case, so do not silence it with an exclamation mark unless you can prove there is a match. Pick the fix by intent: optional chaining with a fallback through ?? when a missing value is normal, or an early throw or return when it means a bug, after which the variable is narrowed. Note that ?? falls back only on null and undefined, while || also replaces 0 and the empty string, a common cause of wrong default values.
type Emp = { id: number; name: string };
const team: Emp[] = [
{ id: 1, name: "Anil" },
{ id: 2, name: "Sara" },
];
const missing = team.find(e => e.id === 3); // Emp | undefined
// console.log(missing.name); // Error: possibly undefined
console.log(missing?.name ?? "No lead"); // No lead
const lead = team.find(e => e.id === 2);
if (!lead) throw new Error("Lead missing");
console.log(lead.name); // Sara38. How do you use an npm package that has no TypeScript types?
First check whether the package ships its own types or has a community package in the @types scope; install that as a dev dependency and you are done. If neither exists, write a declaration file with declare module and the package name, and describe only the functions you actually use, as here. The quick fix, a declare module line with no body, makes every import from that package any; acceptable for a day, not as a permanent state. Make sure the .d.ts file sits inside the folders your tsconfig includes, or the compiler will not see it.
// src/types/pincode-lookup.d.ts
declare module "pincode-lookup" {
export interface PincodeInfo {
pincode: string;
district: string;
state: string;
}
export function lookup(pin: string): PincodeInfo | undefined;
}39. How do you start migrating a JavaScript project to TypeScript?
Migrate in steps so the app keeps shipping. Add a tsconfig with allowJs so JavaScript and TypeScript files can live together, then rename files to .ts one at a time, starting with leaf modules such as utilities and API types that other files import. Begin with loose settings, switch on noImplicitAny and then strictNullChecks once the error count is manageable, and finally strict. JSDoc type comments plus checkJs give you checking in files you have not renamed yet. Track how many JavaScript files and explicit any types remain, and keep big refactors out of the migration.
40. What is structural typing, and why does an extra property sometimes cause an error?
TypeScript compares types by shape, not by name: any value that has at least the required properties is accepted. That is structural typing, and it is why p3, which has an extra z, can be passed as a Point. The exception is the excess property check: when you pass a fresh object literal directly, TypeScript reports unknown properties, because an extra key typed right there is almost always a mistake, such as emial instead of email. This explains why moving a literal into a variable makes the error disappear; it is a deliberate rule, not a compiler bug.
type Point = { x: number; y: number };
function plot(p: Point) { console.log(p.x, p.y); }
const p3 = { x: 1, y: 2, z: 3 };
plot(p3); // fine: has x and y
plot({ x: 1, y: 2, z: 3 }); // Error: z is not in PointReady to check yourself? Take the free TypeScript mock test
TypeScript interview questions: Hard questions (experienced)
20 questions41. How do conditional types and the infer keyword work?
A conditional type, T extends U ? X : Y, picks one of two types depending on whether T is assignable to U, like a ternary at the type level. Inside the extends clause, infer declares a new type variable that the compiler fills in by matching, so ElementOf pulls out the element type of an array and FirstArg pulls out the first parameter of a function. Built-ins such as ReturnType, Parameters and Awaited are written this way. Keep them readable: give each helper a name, test it with sample types, and remember that a conditional on a generic T stays unresolved until T is known.
type ElementOf<T> = T extends readonly (infer U)[] ? U : never;
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type FirstArg<F> =
F extends (first: infer A, ...rest: any[]) => any ? A : never;
type A = ElementOf<string[]>; // string
type B = Unwrap<Promise<number>>; // number
type C = FirstArg<(id: number) => void>; // number42. What is a distributive conditional type, and how do you turn distribution off?
When a conditional type checks a naked type parameter and receives a union, the condition is applied to each member separately and the results are joined back into a union. That is why ToArray of string | number gives string[] | number[] instead of one mixed array, and it is how Exclude and Extract work, as Without shows. To stop it, wrap both sides in a one-element tuple, as in [T] extends [unknown]. Another surprise: never is the empty union, so a distributive conditional applied to never returns never without evaluating either branch.
type ToArray<T> = T extends unknown ? T[] : never;
type NoDistribute<T> = [T] extends [unknown] ? T[] : never;
type A = ToArray<string | number>; // string[] | number[]
type B = NoDistribute<string | number>; // (string | number)[]
type Without<T, U> = T extends U ? never : T;
type C = Without<"a" | "b" | "c", "a">; // "b" | "c"43. How do mapped types with key remapping and template literal types work?
A mapped type loops over a union of keys, usually keyof T, and builds one property per key. Since TypeScript 4.1, an as clause can rename each key, and a template literal type can build the new name from the old one, so name becomes getName. Mapping a key to never drops it, which is how OnlyStrings filters properties by their value type. The string & K part is needed because keyof can also give number and symbol keys, which Capitalize does not accept. These patterns type event handler maps and generated API clients without writing each key by hand.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Candidate = { name: string; experience: number };
type CandidateGetters = Getters<Candidate>;
// { getName: () => string; getExperience: () => number }
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type S = OnlyStrings<Candidate>; // { name: string }44. How do you remove readonly and optional modifiers in a mapped type?
Mapped types can add or remove the readonly and optional modifiers with a plus or minus prefix: -readonly strips read-only and -? strips optionality, which is exactly how the built-in Required is defined. A homomorphic mapped type, one that maps over keyof T, keeps each property's existing modifiers unless you change them, which is why Mutable leaves timeoutMs optional. Removing the question mark with -? also removes the undefined that optionality added. These helpers are shallow: nested objects keep their modifiers unless you recurse, and recursive types cost compile time.
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Concrete<T> = { [K in keyof T]-?: T[K] };
type Config = {
readonly apiUrl: string;
readonly timeoutMs?: number;
};
type Editable = Mutable<Config>;
// { apiUrl: string; timeoutMs?: number }
type Full = Concrete<Config>;
// { readonly apiUrl: string; readonly timeoutMs: number }45. Where does never show up beyond functions that throw, and why is it useful?
never is the bottom type: it has no values, it is assignable to every type, and nothing except never is assignable to it, not even any. It appears as the result of impossible intersections such as string & number, as what remains after narrowing has removed every member of a union, and as the empty result of conditional filters like Exclude. In a union it vanishes, which is what lets mapped and conditional types drop members. In application code its main use is exhaustiveness: a call to a helper with a never parameter compiles only if every case was handled.
46. What does strictFunctionTypes check, and why are method declarations treated differently?
Function parameters should be contravariant: a handler that only understands MouseEvent is not safe where any Event may arrive, because it would read clientX from a keyboard event. With strictFunctionTypes on, which strict includes, TypeScript enforces this for function-typed properties, so assigning onClick to SafeBus.emit is an error. Parameters of members written with method syntax stay bivariant, for compatibility with existing code such as the DOM and array types. So in your own interfaces, declare callbacks as properties with arrow syntax when you want the stricter, safer check.
type Handler = (e: MouseEvent) => void;
interface Bus { emit(e: Event): void } // method
interface SafeBus { emit: (e: Event) => void } // property
const onClick: Handler = e => console.log(e.clientX);
const bus: Bus = { emit: onClick }; // allowed (bivariant)
const safe: SafeBus = { emit: onClick }; // Error with strict47. How do you stop IDs of different entities being mixed up when they are all strings?
Because typing is structural, two aliases of string are interchangeable, so passing a user ID where an order ID is expected compiles. A branded type intersects the base type with a marker property that never exists at runtime, making UserId and OrderId incompatible with each other while both stay usable as strings. Create branded values only through small constructor functions, ideally ones that also validate the format. The brand is erased, so there is no runtime cost. The trade-off is some friction at boundaries such as JSON parsing, where you must convert values explicitly.
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
const toUserId = (s: string) => s as UserId;
const toOrderId = (s: string) => s as OrderId;
function cancelOrder(id: OrderId) { return "cancelled " + id; }
const uid = toUserId("U-1001");
const oid = toOrderId("O-5512");
console.log(cancelOrder(oid)); // cancelled O-5512
// cancelOrder(uid); // Error: UserId is not OrderId48. Since types are erased, how do you keep runtime validation and static types in sync?
Because annotations vanish at runtime, an interface plus separately written checks will drift apart over time. The robust approach is one source of truth: define the schema as a runtime value and derive the static type from it with mapped and indexed access types, as this small example does; schema validation libraries apply the same idea at scale. Validate at every trust boundary: API responses, request bodies, local storage, environment variables and queue messages. Inside the boundary, rely on the static types and skip repeated checks, which keeps the code both safe and fast.
const schema = { name: "string", age: "number" } as const;
type Kinds = { string: string; number: number };
type FromSchema<S> = {
-readonly [K in keyof S]: Kinds[S[K] & keyof Kinds];
};
type Person = FromSchema<typeof schema>;
// { name: string; age: number }
function parsePerson(x: unknown): Person {
if (typeof x !== "object" || x === null) {
throw new Error("Not an object");
}
const o = x as Record<string, unknown>;
for (const [key, kind] of Object.entries(schema)) {
if (typeof o[key] !== kind) throw new Error("Bad " + key);
}
return o as Person;
}
console.log(parsePerson({ name: "Divya", age: 29 }).name);
// Divya49. Node.js can now run .ts files directly. What does it do, and which syntax breaks it?
Node.js 24 runs a .ts file by stripping the type syntax and executing what remains. It does no type checking, so you still run tsc --noEmit in CI. Stripping only works for erasable syntax, meaning code that is valid JavaScript once the types are deleted. Enums, namespaces with values and constructor parameter properties need real code generation, so Node rejects them in its default mode with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. TypeScript 5.8 added the erasableSyntaxOnly option, which reports these constructs at compile time so your code stays runnable by type-stripping tools.
// greet.ts, run with: node greet.ts (Node.js 24)
type Lang = "hi" | "en";
const greet = (l: Lang) => (l === "hi" ? "Namaste" : "Hello");
console.log(greet("hi")); // Namaste
// enum Lang2 { Hi, En }
// Node.js 24 refuses this in its default strip-only mode:
// ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX50. What are isolatedModules and verbatimModuleSyntax, and why do bundler-based projects need them?
Tools such as esbuild, SWC, Babel and Node's type stripping compile each file on its own, without the full type graph, so they cannot tell whether an imported name is a type, which must be erased, or a value, which must be kept. isolatedModules makes tsc report code that a single-file compiler cannot handle safely, such as re-exporting a type without the type keyword. verbatimModuleSyntax, added in TypeScript 5.0, goes further: an import without the type modifier is always kept and one with it is always dropped, so what you write is what is emitted.
// money.ts
export interface Invoice { id: string; totalPaise: number }
export const formatInr = (p: number) =>
"Rs " + (p / 100).toFixed(2);
// billing.ts
import type { Invoice } from "./money"; // erased completely
import { formatInr } from "./money"; // kept as an import
export type { Invoice }; // type-only re-export51. How do you extend a third-party type, such as adding the logged-in user to the Express request?
Use declaration merging through module augmentation: write declare module with the name of the module that defines the interface, then reopen the interface and add your fields. In the Express types, Request is defined in express-serve-static-core, which is why the augmentation targets that module rather than express. For globals such as Window, use declare global inside a file that is a module, which is why the empty export is there. Augmentation only works on interfaces, not type aliases, and the file must be included by tsconfig. Keep the field optional unless every request has it.
// src/types/express.d.ts
import "express";
declare module "express-serve-static-core" {
interface Request {
user?: { id: string; orgId: number; role: "admin" | "staff" };
}
}
// src/types/global.d.ts
export {};
declare global {
interface Window {
dataLayer: unknown[];
}
}52. How do you publish a TypeScript library so its types work for every consumer?
Compile with declaration enabled so tsc writes .d.ts files next to the JavaScript, and add declarationMap, shipping the sources too, so go-to-definition opens your source. In package.json, point the exports map at both the code and the types; inside each condition put types first, because conditions are matched in order. Consumers using moduleResolution node16, nodenext or bundler read exports, while older setups read only the top-level types field, so many packages set both. Never publish types that import from your devDependencies, which consumers will not have.
53. How do you set up TypeScript in a large monorepo so builds stay fast?
Split the code into projects with project references. Each referenced project sets composite, which turns on declaration output and requires every source file to be listed by include or files; dependent projects then read its .d.ts output instead of re-checking its source. tsc --build compiles the projects in dependency order and skips the ones that have not changed, and incremental builds save their state in a build info file so later runs reuse work. skipLibCheck, which skips checking declaration files, often saves a lot of time, at the cost of missing conflicts between library types.
54. The editor is slow and tsc takes minutes. How do you find and fix expensive types?
Measure first. tsc --extendedDiagnostics prints the time spent parsing, binding and checking, plus counts of types and instantiations, and --generateTrace records which files and expressions cost the most. Common culprits are deeply recursive conditional types, huge unions such as template literal types multiplied together, big object types built from long intersections, and large inferred return types reused across many files. Fixes include explicit return types on exported functions, interface extends instead of long intersections and smaller unions. Re-measure after every change.
55. How do you write a generic React component, such as a list that works for any item type?
Make the props type generic and the component a generic function. TypeScript then infers T from the items prop at each use, so getKey and renderItem receive a fully typed item with no annotations. This is one reason plain function components are easier here than React.FC, which has no place for a type parameter. In a .tsx file, a generic arrow function needs a hint so its angle bracket is not read as JSX: a trailing comma after the type parameter, or an extends unknown constraint. To reuse another component's props, use React.ComponentProps applied to typeof that component.
import type { ReactNode } from "react";
type ListProps<T> = {
items: T[];
getKey: (item: T) => string;
renderItem: (item: T) => ReactNode;
};
function List<T>({ items, getKey, renderItem }: ListProps<T>) {
return (
<ul>
{items.map(i => <li key={getKey(i)}>{renderItem(i)}</li>)}
</ul>
);
}
// T is inferred as { id: string; city: string }
<List
items={[{ id: "1", city: "Indore" }]}
getKey={c => c.id}
renderItem={c => c.city}
/>;56. How would you design a typed API client where each endpoint has its own params and response type?
Describe every endpoint once in a type map from path to params and response, and write one generic function whose path parameter is constrained to the map's keys. Indexed access types then give each call the right params and response type, so a misspelt path or a missing parameter is a compile error, and a new endpoint is one new line in the map. The response is still trusted, not checked: the res.json() line is where a runtime validator belongs. In bigger systems, generate the map from an OpenAPI spec so the client and the server cannot drift apart.
type Job = { id: number; title: string };
type Endpoints = {
"/jobs": { params: { city: string }; response: Job[] };
"/jobs/detail": { params: { id: number }; response: Job };
};
async function api<P extends keyof Endpoints>(
path: P,
params: Endpoints[P]["params"],
): Promise<Endpoints[P]["response"]> {
const qs = new URLSearchParams(
Object.entries(params).map(([k, v]) => [k, String(v)]),
);
const res = await fetch(`${path}?${qs}`);
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json(); // validate here in production code
}
// const jobs = await api("/jobs", { city: "Hyderabad" });57. Your Pune IT services team must move a large JavaScript React codebase to strict TypeScript. How would you plan it?
Treat it as an incremental project with measurable milestones, never a big-bang rewrite. Start with allowJs and a loose config so everything builds, then convert shared leaf modules and API types first, since every other file benefits from them. Tighten in stages: noImplicitAny, then strictNullChecks, then full strict, fixing one area at a time; project references can hold converted packages to the higher bar while the rest catches up. Track JavaScript file counts, explicit any and suppression comments in CI so the numbers only go down, and add runtime validation at the boundaries as you go.
58. When is it acceptable to use @ts-ignore, @ts-expect-error or a type assertion, and how do you keep them under control?
Both comments suppress the error on the next line, but @ts-expect-error itself becomes an error when there is nothing left to suppress, so it gets removed as soon as the real problem is fixed. @ts-ignore stays forever and silently hides any new error on that line too. Use suppressions only for wrong third-party types or code in the middle of a migration, always with a reason and a ticket number. Assertions, and especially double casts through unknown, deserve the same scrutiny. Lint rules can ban @ts-ignore and require descriptions, and CI can count suppressions.
59. What problems do const type parameters and NoInfer solve in generic functions?
A const modifier on a type parameter, added in TypeScript 5.0, makes inference behave as if the caller had written as const, so routes keeps the exact literal tuple instead of widening it to string[]. Use a readonly array constraint; with plain string[], TypeScript 5.9 keeps the literals but drops readonly. NoInfer, added in 5.4, marks a parameter that must not take part in inference. Without it the compiler would infer C from both arguments and widen it to include cash, accepting a bad fallback. Both tell the compiler which argument is the source of truth, mostly in library-style helpers.
function routes<const T extends readonly string[]>(p: T): T {
return p;
}
const r = routes(["/home", "/jobs"]);
// readonly ["/home", "/jobs"]
function pick<C extends string>(opts: C[], fallback: NoInfer<C>) {
return opts[0] ?? fallback;
}
pick(["UPI", "card"], "UPI"); // fine
// pick(["UPI", "card"], "cash"); // Error: not "UPI" | "card"60. What changed in TypeScript 5.5 about filter and type predicates, and where does inference still fall short?
TypeScript 5.5 added inferred type predicates: when a function body narrows its parameter and returns that check, the compiler infers a guard such as p is string, so filter returns string[] with no hand-written guard. The rule is strict: a predicate is inferred only if true means the type is narrowed and false means it is not. A truthiness check like !!p does not qualify, because an empty string is also falsy, so that result stays (string | undefined)[]. Before 5.5 you had to write the guard yourself. Keep explicit guards for anything more complex than a single check.
const pins = ["560001", undefined, "411001", undefined];
const valid = pins.filter(p => p !== undefined);
// TypeScript 5.5+: string[]
// before 5.5: (string | undefined)[]
const short = pins.filter(p => !!p);
// still (string | undefined)[]: "" is falsy too
console.log(valid.length, short.length); // 2 2Practice questions for November 2026
New every month3 questions from the TypeScript mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.
-
What is the type Stock equivalent to?
type City = "pune" | "jaipur"; type Stock = Record<City, number>;- { pune: number; jaipur: number }
- number[]
- { pune?: number; jaipur?: number }
- { [key: string]: City }
Show the answer
Answer: { pune: number; jaipur: number }. Record<K, V> makes an object type with one property for each key in K, all of type V. Both keys are required, so leaving out jaipur is an error. That makes Record over a union of keys good for lookup tables.
-
With exactOptionalPropertyTypes on, what happens?
interface Prefs { theme?: "dark" | "light"; } const p: Prefs = { theme: undefined };- It compiles, because optional properties always accept undefined
- A runtime error when p is created
- A compile error: an optional property may be left out, but it may not be set to undefined
- theme becomes "dark" by default
Show the answer
Answer: A compile error: an optional property may be left out, but it may not be set to undefined. exactOptionalPropertyTypes separates a missing key from a key holding undefined. To allow both, write theme?: "dark" | "light" | undefined. It is not part of strict and must be turned on by itself.
-
In TypeScript 5.2 and later, what is the type of big?
declare const ids: string[] | number[]; const big = ids.filter((id) => String(id).length > 3);- string[] | number[]
- (string | number)[]
- A compile error: filter cannot be called on this union
- never[]
Show the answer
Answer: (string | number)[]. Before 5.2, calling filter on a union of array types often failed because the signatures could not be combined. Now TypeScript treats the receiver as (string | number)[] for the call, so the result has that type.
Ready to apply?
There are 1,052 open jobs asking for TypeScript on CoreHRx right now. Create a free profile once and apply with it.