Skip to main content

TypeScript Advanced Types

TypeScript has an advanced type system. It lets you describe complex relationships between types with precision.

These features help you build applications that are robust, easy to maintain, and safe from many type errors.


Advanced TypeScript Types

This page gives an overview of the main advanced type features:

  • Mapped Types: transform the properties of an existing type
  • Conditional Types: pick a type based on a condition
  • Template Literal Types: build string types with template syntax
  • Utility Types: built-in helpers for common transformations
  • Recursive Types: types that refer to themselves, for tree-like data
  • Type Guards and Type Predicates: checks that narrow a type at runtime
  • Type Inference: pattern matching inside types with infer

Mapped Types

A mapped type creates a new type by walking over the properties of an existing type and transforming each one.

Basic Mapped Type

One template changes every property of an object type into a new type. Here every property of User becomes a boolean.

Prompt
Show a basic mapped type that turns every property of an object type into boolean. Declare a generic Flags<T> mapped over keyof T, a User interface with id: number, name: string and email: string, and a UserFlags = Flags<User> alias; add a comment showing the equivalent object type. The file only declares types and prints nothing.
Output: TypeScript file and captured output

Mapped Type Modifiers

While mapping, you can add or remove the readonly and ? (optional) modifiers on every key. A + adds a modifier and a - removes it.

Prompt
Show how a mapped type adds or removes the readonly and ? modifiers. Declare a Todo interface (title: string, description: string, completed: boolean), an OptionalTodo mapped type that makes every property optional, a generic Concrete<T> that strips both readonly and ? with -readonly and -?, and a ReadonlyRequired<T> that adds +readonly and removes ?. Only type declarations, no output.
Output: TypeScript file and captured output

Key Remapping

With the as clause you can rename keys while mapping, or filter keys out by mapping them to never. String helpers like Capitalize and conditional checks make this possible.

Prompt
Show key remapping in a mapped type with the as clause. Declare a User interface (id: number, name: string, email: string), a generic Getters<T> that renames each key to get plus the capitalized key name (using Capitalize<string & K>) with a () => T[K] value, a UserGetters = Getters<User> alias with a comment showing the resulting shape, and a MethodsOnly<T> type that keeps only keys whose values extend Function by mapping the others to never. Types only, prints nothing.
Output: TypeScript file and captured output

Conditional Types

A conditional type chooses between two types depending on a condition. It works like an if statement, but at the type level.

Basic Conditional Types

The form T extends U ? X : Y picks X when T is assignable to U, and Y otherwise. The check happens entirely at the type level.

Prompt
Show a basic conditional type and one that uses infer. Declare IsString<T> as T extends string ? true : false, then aliases A, B, C, D for string, number, the literal 'hello' and string | number, with comments noting the results true, false, true and boolean. Also declare ArrayElement<T> that infers the element type U of an array type and a Numbers = ArrayElement<number[]> alias. Types only, no output.
Output: TypeScript file and captured output

Infer Keyword

Inside the condition of a conditional type, infer introduces a new type variable and captures part of the type being checked. This is how the built-in ReturnType and Parameters helpers work.

Prompt
Show how the infer keyword captures parts of a type inside a conditional type by re-creating four built-in helpers. Declare ReturnType<T> (infers the return type R of a function type, else any), Parameters<T> (infers the parameter tuple P, else never), ConstructorParameters<T> constrained to constructor types and inferring the constructor parameter tuple, and InstanceType<T> inferring the instance type of a constructor. Types only, nothing is printed; make the file a module so the names do not clash with the built-in globals.
Output: TypeScript file and captured output

Distributed Conditional Types

When the checked type is a bare type parameter, a conditional type is applied to each member of a union separately. This is called distribution. Wrapping the type in a tuple, like [T] extends [any], stops the distribution so the union is handled as one type.

Prompt
Show how conditional types distribute over union types, and how to stop the distribution. Declare ToArrayNonDist<T> as T extends any ? T[] : never and StrOrNumArr = ToArrayNonDist<string | number>, then ToArray<T> written with the tuple wrapper [T] extends [any] ? T[] : never and StrOrNumArr2 = ToArray<string | number>, each with a comment showing the resulting type. Finally declare FilterStrings<T> that keeps only string members and a Letters alias applied to 'a' | 'b' | 1 | 2 | 'c'. Types only, no output.
Output: TypeScript file and captured output

Template Literal Types

Template literal types build new string types using the same backtick syntax as JavaScript template strings.

Basic Template Literal Types

A template literal type limits a string to a pattern. Combined with unions, it produces every possible combination of the parts. Assigning a string that does not match the pattern is a compile error.

Prompt
Show basic template literal types and the compile error they catch. Declare a Greeting type equal to the template `Hello, ${string}`, assign the valid value 'Hello, World!' to validGreeting, and assign 'Hi there!' to invalidGreeting marked // Error so the file deliberately fails type-checking. Then declare Color ('red' | 'green' | 'blue') and Size ('small' | 'medium' | 'large') unions and a Style template type `${Color}-${Size}` with a comment listing the nine resulting members. Expected result: a compile error for the invalid greeting.
Output: TypeScript file and captured output

String Manipulation Types

TypeScript ships four helpers that change the case of string literal types: Uppercase, Lowercase, Capitalize and Uncapitalize. They are handy inside template literal types.

Prompt
Show the built-in string manipulation types applied to string literals. Declare T1 to T4 using Uppercase<'hello'>, Lowercase<'WORLD'>, Capitalize<'typescript'> and Uncapitalize<'TypeScript'> with comments showing the results, then an EventType union ('click' | 'change' | 'keydown') and an EventHandler template type that prefixes on to the capitalized event names. Types only, no output.
Output: TypeScript file and captured output

Advanced Patterns

Template literal types can be combined with infer and key remapping. That lets you pull information out of a string type, such as route parameters, or generate a whole API of handler names from an event map.

Prompt
Show advanced template literal patterns that combine infer and key remapping. Declare a recursive ExtractRouteParams<T> that matches `${string}:${infer Param}/${infer Rest}` (and the single-parameter form) to build an object type with one string property per route parameter, plus a Params alias for '/users/:userId/posts/:postId'. Then declare an EventMap type (click with x/y numbers, change as string, keydown with key: string and code: number) and an EventHandlers mapped type that renames each key to on plus the capitalized name and types it as a handler receiving EventMap[K]. Types only, no output.
Output: TypeScript file and captured output

Utility Types

TypeScript comes with many built-in utility types. They cover the most common type transformations so you do not have to write them yourself.

Common Utility Types

Partial, Required, Readonly, Pick and Omit are the ones you will use most. You can also read a single property type with User['id'] and all key names with keyof User.

Prompt
Show the most common built-in utility types applied to one interface. Declare a User interface with id: number, name: string, email: string and createdAt: Date, then aliases PartialUser = Partial<User>, RequiredUser = Required<PartialUser>, ReadonlyUser = Readonly<User>, UserPreview = Pick<User, 'id' | 'name'> and UserWithoutEmail = Omit<User, 'email'>, each with a short comment. Finish with UserId = User['id'] and UserKeys = keyof User and comments showing the results. Types only, no output.
Output: TypeScript file and captured output

Advanced Utility Types

Exclude removes members from a union and Extract keeps only the matching ones. You can also write your own helpers, like a Mutable type that strips readonly from every property.

Prompt
Show union-filtering utility types and a custom mapped helper. Declare a NonNullable<T> conditional type that maps null | undefined to never, a Numbers union 1 | 2 | 3 | 'a' | 'b', JustNumbers = Exclude<Numbers, string> and JustStrings = Extract<Numbers, string> with result comments, then object types A (a: string; b: number; c: boolean) and B (a: string; b: number) and C = Omit<A, keyof B>, and finally a Mutable<T> mapped type using -readonly. Types only, no output; make the file a module so NonNullable does not clash with the global one.
Output: TypeScript file and captured output

Recursive Types

A recursive type refers to itself in its own definition. This is the natural way to describe tree-like data, where a node can contain more nodes of the same shape.

Basic Recursive Type

A binary tree node holds optional left and right nodes of the same type. A JSON value can contain arrays and objects of more JSON values. A comment holds an array of replies that are also comments.

Prompt
Show basic recursive types that model tree-like data. Declare a generic BinaryTree<T> with a value and optional left/right subtrees of the same type, a JSONValue union of string, number, boolean, null, JSONValue[] and an object with JSONValue values, and a Comment type with id, content, a replies: Comment[] array and createdAt: Date. Types only, no output; make the file a module so Comment does not clash with the DOM global.
Output: TypeScript file and captured output

Advanced Recursive Types

The same idea describes a linked list, a directory tree whose children are files or more directories, a state machine whose transitions lead to other states, and even a function type that accepts itself.

Prompt
Show more advanced recursive types. Declare a generic LinkedList<T> with value and next: LinkedList<T> | null, a File type (type: 'file', name, size) and a Directory type (type: 'directory', name, children: (File | Directory)[]), a State type whose transitions map event names to other States, and a RecursiveFunction<T> function type that accepts T | RecursiveFunction<T>. Types only, no output; make the file a module so File does not clash with the DOM global.
Output: TypeScript file and captured output

Best Practices

When to use advanced types:

  • Use mapped types when you need to change many properties of an object type at once
  • Use conditional types when one type depends on another type
  • Use template literal types for string patterns and string matching
  • Use utility types for common transformations — prefer the built-in ones when they fit
  • Use recursive types for tree-like or nested data

Performance considerations:

  • Deeply nested recursive types can slow down the TypeScript compiler
  • Very large union types (100+ members) can cause performance problems
  • Break complex types into smaller type aliases

Common Pitfalls

Type inference issues:

  • Conditional types distribute over union types, which can be surprising
  • infer behaves differently depending on where it is used
  • Some utility types do not work well with any or unknown

Maintainability:

  • Overusing complex types makes code hard to read
  • Explain complex type transformations with comments
  • For very complex cases, a type assertion or a helper function can be simpler