Skip to content
Laviyx Learn

Try

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.

Advanced6 min readUpdated
On this page

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
TypeScript
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);
Output
{ 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:

TypeScript
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);
Output
{ 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:

TypeScript
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")));
Output
loading user u-42

Passing 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:

TypeScript
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);
Output
8 2

Overloads 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:

TypeScript
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);
Output
localhost

Assigning 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?