Think of TypeScript’s type system like a smart label-maker for boxes in a warehouse. Most labels are simple: "books", "tools", or "fragile". Advanced types are the label-maker’s special modes that can inspect a box, rewrite the label, or even build new labels from old ones.
That matters when your data isn’t flat. Real programs have nested objects, unions, arrays, and APIs that come back in different shapes. Instead of writing lots of manual types, you can teach the type system rules for transforming one shape into another.
These features let you do type-level programming without running code. You can ask questions like: “If this is an array, what is its item type?”, “Which keys are optional?”, or “Can I create event names from object keys?”
What Advanced Types Are and Why They Exist
Advanced types are TypeScript features that let types depend on other types. The main tools are conditional types, mapped types, template literal types, keyof, indexed access types, and recursive types.
They matter because they reduce duplication and keep types in sync with values. Instead of hand-writing many helper interfaces, you describe transformations once and let the compiler derive the rest.
Interviewers use these topics to see if you understand that TypeScript types are not just annotations. They form a small compile-time language for expressing rules over data shapes, unions, and property names.
Conditional Types and `infer`: Type-Level Branching
A conditional type chooses one type or another based on whether a type extends a constraint: T extends U ? X : Y. This is the type-level version of an if statement.
The important detail is that when T is a union, conditional types can distribute over each member. That means A | B may be transformed into separate results for A and B, which is often exactly what you want.
The infer keyword lets you capture part of a matched type. For example, you can extract the return type of a function or the item type of an array by pattern-matching on the structure inside the conditional.
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number>; // number
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
`keyof` and Indexed Access: Reading Property Information
The keyof operator turns an object type into a union of its property names. If Person has name and age, then keyof Person becomes 'name' | 'age'.
Indexed access types let you read a property type by key, like Person['name']. Combined with keyof, they let you build helpers that work over all properties or specific subsets.
This is the foundation for many utilities. Once you can say “all keys” and “the type at this key,” you can transform object types systematically instead of copying fields by hand.
type Person = { name: string; age: number };
type Keys = keyof Person; // 'name' | 'age'
type NameType = Person['name']; // string
type ValueOf<T> = T[keyof T];
type PersonValues = ValueOf<Person>; // string | number
Mapped Types: Rebuilding Object Types Key by Key
A mapped type iterates over a union of keys and creates a new object type from them: { [K in keyof T]: ... }. It is like a loop over property names at the type level.
Mapped types are useful for making properties optional, readonly, nullable, or transformed in some uniform way. You can also remap keys, which is how you change object property names rather than just their values.
This is where type-level programming starts feeling practical. Instead of defining Partial, Readonly, or Pick by hand each time, you express the transformation once and reuse it everywhere.
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type Person = { name: string; age: number };
type P = MyPartial<Person>; // { name?: string; age?: number }
Template Literal Types: Building String Patterns from Types
Template literal types let you combine string literal types to form new string literal types, like `get${Capitalize<K>}`. They are the type-level version of string interpolation.
This becomes powerful when paired with keyof and mapped types. You can derive event names, API method names, or prefixed keys from an existing object type, and the compiler will keep those names consistent.
They also work with unions, so a union of keys turns into a union of all generated strings. That makes them perfect for strongly typed naming conventions.
type Events<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void
};
type Person = { name: string; age: number };
type PersonEvents = Events<Person>;
Recursive Types: Repeating a Rule Until the Shape Is Done
A recursive type refers to itself to process nested structures. This is how you model trees, JSON-like values, or deeply nested arrays and objects.
The key is to define a clear base case, just like recursion in runtime code. Without a base case, the type will either become uselessly broad or hit compiler recursion limits.
Recursive types often combine everything else: conditional types to decide what to do next, infer to extract a part, and mapped types to rebuild the result at each level.
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type Data = { user: { name: string; tags: string[] } };
type R = DeepReadonly<Data>;
Realistic Example: Typed Event API from a Model
Suppose you have a model object and want event handlers that stay in sync with its fields. You can derive handler names from keys, and each handler receives the right value type.
Start with a model like { name: string; age: number }. Use keyof to get the field names, a mapped type to iterate over them, and a template literal type to generate names like onName and onAge.
If you later rename a field, the event API changes automatically. That is the main win of advanced types: the compiler becomes a refactoring partner instead of a passive checker.
type Model = { name: string; age: number };
type Handlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void
};
type H = Handlers<Model>;
// onName: (value: string) => void
// onAge: (value: number) => void
Realistic Example: Deep Unwrapping a Promise or Array
A common interview pattern is extracting the “inner” type from nested wrappers. With infer, you can write a type that unwraps a Promise, an array, or leaves the type alone if neither matches.
That demonstrates conditional types plus pattern matching. A Promise<string> becomes string, number[] becomes number, and boolean stays boolean.
This is useful because it shows the compiler can reason about structure, not just names. The same idea scales to more complex recursive helpers for nested containers.
type Unwrap<T> = T extends Promise<infer U>
? U
: T extends Array<infer U>
? U
: T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<number[]>; // number
type C = Unwrap<boolean>; // boolean