TS

TypeScript Fundamentals

17 lessons

Progress0%
1. Introduction to TypeScript
1What is TypeScript?2Setting Up TypeScript
2. Basic Types
1Primitive Types2Interfaces and Type Aliases
3. Functions and Generics
Typed FunctionsGenerics
4. Classes and OOP
ClassesInheritance and Interfaces
5. Advanced Types
Union and Intersection TypesUtility Types
6. Modules and Decorators
ES Modules
7. Decorators
Class & Method DecoratorsProperty & Parameter Decorators
8. Declaration Files
Writing .d.ts Files@types & DefinitelyTyped
9. Advanced Patterns
Conditional Types & inferTemplate Literal Types & satisfies
All Tutorials
TypeScriptAdvanced Patterns
Lesson 16 of 17 min
Chapter 9 · Lesson 1

Conditional Types & infer

Conditional types bring if/else logic into the TypeScript type system. They let you compute types dynamically based on relationships between other types, unlocking a whole class of utility types that would otherwise be impossible to express.

Conditional Type Syntax

typescript
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>;   // "yes"
type B = IsString<number>;   // "no"

Read this as: "If T is assignable to string, produce 'yes', otherwise produce 'no'."

Distributive Conditional Types

When the checked type is a bare type parameter and you pass a union, the conditional type distributes over each member:

typescript
type ToArray<T> = T extends any ? T[] : never;
type Arr = ToArray<string | number>; // string[] | number[]

To disable distribution, wrap the type parameter in a tuple:

typescript
type NoDistrib<T> = [T] extends [any] ? T[] : never;

The infer Keyword

infer introduces a new type variable within the extends clause. TypeScript infers its value when the pattern matches:

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

Built-in Utility Types Using infer

TypeScript's own standard library uses infer extensively:

  • ReturnType<T> — return type of a function
  • Parameters<T> — parameter tuple of a function
  • ConstructorParameters<T> — constructor parameter tuple
  • Awaited<T> — unwraps nested Promise types recursively
typescript
type Awaited<T> =
  T extends null | undefined ? T :
  T extends object & { then(onfulfilled: infer F): any } ?
    F extends (value: infer V) => any ? Awaited<V> : never
  : T;

Building Custom Utility Types

You can compose conditional types to create powerful custom utilities:

typescript
type NonNullable<T> = T extends null | undefined ? never : T;
type Flatten<T> = T extends Array<infer Item> ? Item : T;

Deep Readonly with Recursive Conditionals

Conditional types can be recursive, enabling transformations that descend into arbitrarily deep object structures.

Mastering conditional types and infer takes your TypeScript from merely type-safe to genuinely expressive — capable of encoding complex type-level algorithms that catch entire categories of bugs at compile time.

Code Examples

Simple Conditional Type — Nullable Checktypescript
// IsNullable: true if T includes null or undefined, false otherwise
type IsNullable<T> = null extends T ? true : undefined extends T ? true : false;

type A = IsNullable<string | null>;      // true
type B = IsNullable<string | undefined>; // true
type C = IsNullable<string>;             // false
type D = IsNullable<number | null | undefined>; // true

// Unwrap nullable: strip null and undefined from a union
type NonNullableDeep<T> = T extends null | undefined ? never : T;

type E = NonNullableDeep<string | null | undefined>; // string
type F = NonNullableDeep<number | null>;              // number

// Distributive conditional — maps over each union member
type Stringify<T> = T extends number ? `${T}` : T;
type G = Stringify<1 | 2 | "hello">; // "1" | "2" | "hello"

// Runtime validation helper leveraging the types above
function requireValue<T>(val: T): NonNullableDeep<T> {
  if (val == null) throw new Error("Value is null or undefined");
  return val as NonNullableDeep<T>;
}

console.log(requireValue("hello"));
console.log(requireValue(42));
try {
  requireValue(null);
} catch (e) {
  console.log((e as Error).message);
}

IsNullable uses two nested conditional type checks to detect whether null or undefined is in the union. NonNullableDeep is a distributive conditional that removes those members. Stringify maps numeric literal types to their string equivalents.

infer — Extracting Return and Parameter Typestypescript
// Manual implementation of ReturnType
type MyReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : never;

// Manual implementation of Parameters
type MyParameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

// Flatten an array one level deep
type Flatten<T> = T extends Array<infer Item> ? Item : T;

// Unwrap a Promise
type Awaited<T> = T extends Promise<infer V> ? Awaited<V> : T;

// --- Test cases ---
function fetchUser(id: string): Promise<{ name: string; age: number }> {
  return Promise.resolve({ name: "Alice", age: 30 });
}

type FetchReturn  = MyReturnType<typeof fetchUser>;   // Promise<{name,age}>
type FetchParams  = MyParameters<typeof fetchUser>;   // [id: string]
type ResolvedUser = Awaited<FetchReturn>;              // {name: string; age: number}
type NumberItem   = Flatten<number[]>;                // number
type PassThrough  = Flatten<string>;                  // string (not an array)

// Verify at runtime
async function run() {
  const user = await fetchUser("u-1");
  console.log(`Name: ${user.name}, Age: ${user.age}`);

  const params: FetchParams = ["u-2"];
  console.log("Params tuple:", params);
}

run();

MyReturnType uses infer R in the return position to capture whatever type the function returns. MyParameters uses infer P in the args position to capture the entire parameter tuple. Awaited recursively unwraps nested Promises.

Recursive Conditional Type — DeepReadonlytypescript
type Primitive = string | number | boolean | bigint | symbol | null | undefined;

type DeepReadonly<T> =
  T extends Primitive
    ? T
    : T extends Array<infer Item>
    ? ReadonlyArray<DeepReadonly<Item>>
    : T extends Map<infer K, infer V>
    ? ReadonlyMap<K, DeepReadonly<V>>
    : T extends Set<infer Item>
    ? ReadonlySet<DeepReadonly<Item>>
    : { readonly [K in keyof T]: DeepReadonly<T[K]> };

// Test with a nested structure
interface AppConfig {
  server: {
    host: string;
    port: number;
    tls: { enabled: boolean; cert: string };
  };
  features: string[];
  limits: Map<string, number>;
}

type FrozenConfig = DeepReadonly<AppConfig>;

// FrozenConfig.server.host          → readonly string
// FrozenConfig.server.tls.enabled   → readonly boolean
// FrozenConfig.features              → ReadonlyArray<string>
// FrozenConfig.limits                → ReadonlyMap<string, number>

const config: FrozenConfig = {
  server: { host: "localhost", port: 3000, tls: { enabled: true, cert: "..." } },
  features: ["auth", "logging"],
  limits: new Map([["rps", 1000]]),
};

console.log(config.server.host);
console.log(config.server.tls.enabled);
console.log(config.features[0]);
console.log(config.limits.get("rps"));

DeepReadonly recurses through primitives, arrays, Maps, Sets, and plain objects, applying readonly at every level. The infer keyword extracts element/key/value types from generic containers so they can be recursively wrapped.

Quick Quiz

1. What does the infer keyword do in a conditional type?

2. What is a distributive conditional type?

3. What does type Flatten<T> = T extends Array<infer Item> ? Item : T evaluate to for Flatten<number[]>?

4. How do you prevent a conditional type from distributing over a union?

Was this lesson helpful?

PreviousNext