DEV Community

Cover image for 3 Advanced TypeScript Concepts to keep handy
yoav hirshberg
yoav hirshberg

Posted on

3 Advanced TypeScript Concepts to keep handy

Recursion, Conditionals, and Utility Types for writing concise typed systems that scale.

TypeScript is more than just throwing type annotations in your JavaScript— it’s a superset that empowers you to write safer, more predictable, and maintainable code. Once you’re comfortable with the basics, you'll often encounter scenarios where you'd want some more 'firepower'. In this post we'll explore three powerful concepts (basic TS knowledge is assumed):

  1. Recursive types
  2. Mapped and conditional types
  3. Utility types with type inference

1. Recursive Types

You read it right - Recursion exists in TypeScript too. Recursive types are a technique for describing data structures that can contain themselves, such as trees, linked lists, or deeply nested objects.
One classic example is a type for representing JSON values, which can be strings, numbers, booleans, arrays of JSON values, or objects containing more JSON values.

type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

// Example usage:
const data: JSONValue = {
  name: "Alice",
  age: 30,
  friends: [
    { name: "Bob", age: 31 },
    { name: "Carol", age: 28 }
  ],
  address: null
};
Enter fullscreen mode Exit fullscreen mode

Recursive types let TypeScript describe and enforce the shape of complex, nested data structures, making them both powerful yet simple to read and understand.

Here is another straightforward example:

type TreeNode<T> = {
  value: T;
  children?: TreeNode<T>[];
};

// Example usage:
const tree: TreeNode<string> = {
  value: "root",
  children: [
    {
      value: "child1",
      children: [
        { value: "grandchild1" }
      ]
    },
    {
      value: "child2"
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

2. Mapped Types & Conditional Types

2.1 Mapped Types
Mapped types allow you to create new types by transforming properties of existing ones. You can make all properties optional, readonly, or change their types.

type User = {
  id: number;
  name: string;
  isActive: boolean;
};

// Make all properties optional
type OptionalUser = {
  [K in keyof User]?: User[K];
};

// Make all properties readonly
type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};
Enter fullscreen mode Exit fullscreen mode

TypeScript provides built-in mapped types like Partial<T>, Readonly<T>, and Record<K,T> for common use cases, we'll explore them later on.

2.2 Conditional Types
Just like recursion, conditionals also exist in TypeScript. Conditional types let you choose types based on other types, similar to ternary expressions, but for types.

interface Person {...}
interface User extends Person {...}
interface Bot {...}

type Visitor<T> = T extends Person ? User : Bot;
Enter fullscreen mode Exit fullscreen mode

You can use conditional types to create flexible APIs, filter types, or extract information from types—enabling advanced type computations.

Filtering types:
Suppose you want to exclude all string properties from a type - here, ExcludeStrings uses a conditional type and as mapping to filter out keys whose property type is string.

type ExcludeStrings<T> = {
  [K in keyof T as T[K] extends string ? never : K]: T[K]
};

type User = {
  id: number;
  name: string;
  isActive: boolean;
};

type NonStringProps = ExcludeStrings<User>; 
// Result: { id: number; isActive: boolean }
Enter fullscreen mode Exit fullscreen mode

Extracting Information from Types:
Get the return type of a function

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type MyFunc = (x: number) => Promise<string>;
type Result = ReturnType<MyFunc>; // Result: Promise<string>
Enter fullscreen mode Exit fullscreen mode

Flexible APIs:
Creating a type that adapts its output type based on input type:

type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<number[]>;      // Result: number
type B = Flatten<string>;        // Result: string
type C = Flatten<boolean[][]>;   // Result: boolean[]
Enter fullscreen mode Exit fullscreen mode

3. Utility types

Utility types are built-in helpers for manipulating types:

  • Record<K,T>: Constructs an object type with keys K and values
  • Omit<T,K>: Constructs a type by removing keys K from type T of type T
  • ReturnType<T>: Gets the return type of a function
  • Parameters<T>: Gets the parameter types of a function
interface User {
  id: string;
  name: string;
  email: string;
  friends?: string[];
  meta: {...};
}

type DisplayedUser = Omit<User, 'meta'>;

// you can omit multiple keys using union as a second argument
type LeanUser = Omit<User, 'friends' | 'meta'>;

type UserRecord = Record<number, User>;

const userRecord: UserRecord = {
  1: { id: '1', name: 'Alice', email: 'alice@example.com', friends: [...], meta: {} },
  2: { id: '2', name: 'Bob', email: 'bob@example.com', meta: {} },
};
Enter fullscreen mode Exit fullscreen mode

Type inference and utility types let you write generic, reusable code that is based on your existing types, so you won't have to write additional types.
I personally find that Partial, Record, and Omit, are really useful and used more often, but there are way more, and each has its own use case where it shines.
You can explore the TypeScript docs for all utility types to learn more.

Bonus

Bringing it all together by making a DeepPartial type (for recursively making all properties optional):

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

type Settings = {
  appearance: {
    theme: string;
    fontSize: number;
  };
  isEnabled: boolean;
};

type PartialSettings = DeepPartial<Settings>;
// Result: 
// {
//   appearance?: { theme?: string; fontSize?: number };
//   isEnabled?: boolean;
// };
Enter fullscreen mode Exit fullscreen mode

Conclusion

These concepts are probably not something you will use or encounter every day, but keeping them in the back of your head for the right moment can save you a good amount of scratching your head, and from writing too-complex or redundant types.
Knowing and mastering advanced concepts like these will help you model data more accurately and easily, write cleaner and safer code, and create flexible APIs.

I would love to know if you found it useful, or if you add a special use-case where one of these patterns nailed it for you (or any other pattern/ concept).

Top comments (0)