Skip to main content

TS Ep 41: Conditional Types (The Basics)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 41: This Article
Welcome to the Advanced module! Here, types are no longer static annotations; they become Turing-complete functions that evaluate logic during compilation. The core engine powering this logic is the Conditional Type.

1. The Syntax: T extends U ? X : Y
#

A Conditional Type looks exactly like a JavaScript ternary operator (condition ? true : false). However, instead of evaluating a boolean value at runtime, it evaluates assignability using the extends keyword at compile time.

// "If T is assignable to string, evaluate to true. Else, evaluate to false."
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;      // false
Warning

The extends keyword in conditional types means assignability, not strict equality! If T is a specific string literal (like "hello"), it extends the broader string type, so the condition evaluates to true.


2. Dynamic Return Types (Replacing Overloads)
#

The most practical everyday use case for conditional types is calculating a function’s return type dynamically based on the type of its input argument.

Imagine a database fetch function that takes either a single ID (string) or an array of IDs (string[]).

  • If it receives a string, it returns a single User.
  • If it receives a string[], it returns User[].

Before conditional types, we had to use verbose Function Overloads (from the Fundamental module). With conditional types, we can achieve this in one elegant generic signature!

interface User { id: string; name: string }

// 1. The Conditional Type Logic
type FetchResult<T> = T extends string ? User : User[];

// 2. The Generic Function Signature
// We constrain T so the user can only pass a string or a string array.
function fetchUser<T extends string | string[]>(idOrIds: T): FetchResult<T> {
  // Runtime implementation details...
  return {} as any;
}

// 🟢 The compiler dynamically computes the exact return type!
const single = fetchUser("usr_1"); 
// Inferred Type: User

const multiple = fetchUser(["usr_1", "usr_2"]); 
// Inferred Type: User[]

By linking the generic input T directly to the return type via a conditional evaluation, we eliminate overload boilerplate and create a perfectly typesafe API contract.


3. Nested Conditionals (Else-If Chains)
#

Just like in JavaScript, you can nest ternary operators to create complex else-if chains in the type system. This allows you to construct massive type-level routers.

// A Type-Level router that extracts the primitive name of a type
type GetPrimitiveName<T> = 
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object"; // The fallback

type T1 = GetPrimitiveName<"hello">;       // "string"
type T2 = GetPrimitiveName<99.9>;          // "number"
type T3 = GetPrimitiveName<() => void>;    // "function"
type T4 = GetPrimitiveName<{ id: 1 }>;     // "object"

4. Conditional Types in Real-World Utilities
#

If you recall the Intermediate Module, this exact T extends U ? X : Y syntax is the engine that powers TypeScript’s built-in Extract and Exclude utility types!

// Exclude: If T extends U, return never (delete it). Else, return T.
type CustomExclude<T, U> = T extends U ? never : T;

Summary & Next Steps
#

In this episode:

  • We introduced the ternary syntax of the type system: T extends U ? X : Y.
  • We learned that conditions evaluate based on assignability, not strict equality.
  • We used conditional types to create dynamic function return signatures, eliminating the need for overloads.
  • We built else-if logic chains using nested conditionals.

This episode focused on feeding single types into conditionals. But what happens when you feed a Union Type ("A" | "B") into a generic T that is evaluated conditionally?

In Episode 42: Distributive Conditional Types, we will uncover the most confusing (and powerful) behavior in the TypeScript compiler!

typescript - This article is part of a series.
Part 41: This Article