TypeScript course menu
Advanced TypeScript Interview Questions and Answers
Advanced TypeScript interview questions with tested answers: conditional types and infer, distributive conditional types, mapped types with key remapping, template literal types, variance, branded types, implementing utility types, assertion functions, and project configuration.
On this page
- 1. Implement Pick and Omit yourself.
- 2. How do conditional types and infer work? Implement ReturnType.
- 3. What are distributive conditional types?
- 4. What are branded (nominal) types and why use them?
- 5. Explain variance with an example.
- 6. How would you type a function whose return type depends on its argument?
- 7. What are template literal types used for?
- 8. What does the satisfies operator solve that annotations do not?
- 9. What are assertion functions?
- 10. How do you make an object deeply readonly?
- 11. What compiler options would you enable on a new project, and why?
- 12. Where can TypeScript's guarantees break down?
- Keep going
Advanced questions are common for senior roles and library-heavy codebases. They often ask you to implement a built-in utility type or explain how the type system behaves in edge cases.
1. Implement Pick and Omit yourself.#
Show solution
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyOmit<T, K extends keyof any> = MyPick<T, Exclude<keyof T, K>>;
interface User { id: number; name: string; password: string }
const publicUser: MyOmit<User, "password"> = { id: 1, name: "Asha" };
const idOnly: MyPick<User, "id"> = { id: 2 };
console.log(publicUser, idOnly);{ id: 1, name: 'Asha' } { id: 2 }MyPick is a mapped type over the selected keys; MyOmit picks every key except the excluded ones. (keyof any is string | number | symbol.)
2. How do conditional types and infer work? Implement ReturnType.#
Show solution
T extends U ? X : Y chooses a type based on assignability. infer captures part of a matched type:
type MyReturnType<F> = F extends (...args: never[]) => infer R ? R : never;
function makeUser() {
return { id: 1, tags: ["new"] };
}
const u: MyReturnType<typeof makeUser> = { id: 5, tags: [] };
console.log(u);{ id: 5, tags: [] }3. What are distributive conditional types?#
Show solution
When a conditional type checks a bare type parameter and receives a union, it is applied to each member separately and the results are unioned: ToArray<string | number> with T extends unknown ? T[] : never gives string[] | number[]. This is how Exclude and Extract filter unions. Wrapping in a tuple, [T] extends [U], disables distribution.
4. What are branded (nominal) types and why use them?#
Show solution
TypeScript is structural, so two string aliases (such as user IDs and order IDs) are interchangeable, which can hide bugs. A brand adds a phantom property that only exists in the type:
type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
const toUserId = (id: string) => id as UserId;
function loadUser(id: UserId): string {
return `loading user ${id}`;
}
console.log(loadUser(toUserId("u-42")));loading user u-42Passing a plain string or an OrderId to loadUser is now a compile error, while at runtime the values remain plain strings.
5. Explain variance with an example.#
Show solution
Variance describes how subtyping of a type relates to subtyping of types built from it. Arrays and return types are covariant: a Dog[] can be used where an Animal[] is expected. Function parameters should be contravariant: a function accepting any Animal can be used where a function taking a Dog is needed, but not the reverse. With strictFunctionTypes, TypeScript checks function-type parameters contravariantly (method-style declarations are still checked bivariantly for compatibility). TypeScript also allows explicit variance annotations (in and out) on type parameters.
6. How would you type a function whose return type depends on its argument?#
Show solution
With overloads, or with a generic conditional type:
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]) {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
const a = parse("4"); // number
const b = parse(["1", "2"]); // number[]
console.log(a * 2, b.length);8 2Overloads are usually clearer for a small number of forms.
7. What are template literal types used for?#
Show solution
They build and match string patterns at the type level: event names (`on${Capitalize<Event>}`), CSS values (`${number}px`), route parameters (extracting "id" from "/users/:id" with infer), and renaming keys in mapped types (as `get${Capitalize<K>}` ).
8. What does the satisfies operator solve that annotations do not?#
Show solution
An annotation (const x: T = ...) widens the value to T, losing precise information such as literal values or which union member each property is. satisfies T validates the value against T but keeps the inferred type, so you get both checking and precise auto-complete. as const satisfies T gives readonly literal types that are also validated.
9. What are assertion functions?#
Show solution
Functions whose return type is asserts condition or asserts value is T. They throw when the check fails; after a successful call, TypeScript narrows the value for the rest of the scope. They are useful for validating inputs and configuration at the start of a function or program.
10. How do you make an object deeply readonly?#
Show solution
With a recursive mapped type:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
const config: DeepReadonly<{ db: { host: string; ports: number[] } }> = {
db: { host: "localhost", ports: [5432] },
};
console.log(config.db.host);localhostAssigning to config.db.host is now a compile error. For runtime protection, Object.freeze is still needed (and it is shallow).
11. What compiler options would you enable on a new project, and why?#
Show solution
strict (all core safety checks), noUncheckedIndexedAccess (index reads may be undefined), noImplicitOverride, exactOptionalPropertyTypes if the team accepts its strictness, verbatimModuleSyntax (predictable type-only imports, required by many build tools), skipLibCheck (speed, and avoids errors in third-party .d.ts files), plus the right module/moduleResolution for the runtime (nodenext for Node.js, bundler with a bundler).
12. Where can TypeScript's guarantees break down?#
Show solution
At boundaries and escape hatches: any, type assertions (as), non-null assertions (!), incorrect type guards, JSON.parse and response.json() results, incorrect third-party declaration files, unchecked index access without noUncheckedIndexedAccess, and mutation through aliases of readonly views. Runtime validation at system boundaries closes most of these gaps.
Keep going#
Browse every question with hints on the TypeScript interview page, or review the Web Developer role path.
Finished this lesson?