Ink&Horizon
HomeBlogTutorialsLanguages
Ink&Horizon— where knowledge meets the horizon —Learn to build exceptional software. Tutorials, guides, and references for developers — from first brushstroke to masterwork.

Learn

  • Blog
  • Tutorials
  • Languages

Company

  • About Us
  • Contact Us
  • Privacy Policy

Account

  • Sign In
  • Register
  • Profile
Ink & Horizon

© 2026 InkAndHorizon. All rights reserved.

Privacy PolicyTerms of Service
Back to Blog
Frontend

TypeScript 5.x Generics: From Basics to Wizardry

Master generic types, conditional types, mapped types, infer, template literals, and const type parameters

2026-03-20 24 min read
ContentsWhy Generics Are Non-NegotiableGeneric Functions: The FoundationGeneric Constraints with extendsConditional Types: Type-Level If/ElseThe infer Keyword: Pattern Matching for TypesMapped Types: Transform Every PropertyTemplate Literal Types: String Manipulation at the Type Levelconst Type Parameters (TypeScript 5.0+)Real-World Pattern: Type-Safe API ClientTop Interview Questions on TypeScript GenericsKey Takeaways

Why Generics Are Non-Negotiable

Without generics, you face a choice: use "any" (lose type safety) or write duplicate functions for each type (lose maintainability). Generics let you write functions, classes, and types that work with ANY type while preserving full type information.

Every library you use — React, Prisma, tRPC, Zod, Express — is built on generics. useState<T>, Array<T>, Promise<T>, Map<K,V> — these are all generic types. Understanding generics is the prerequisite for reading library source code and writing reusable code.

TypeScript 5.x has significantly enhanced generics with const type parameters (5.0), higher-order type inference improvements, and better narrowing inside generic functions. These features make the type system more expressive than ever.

Key Takeaways

Generics = type variables that preserve type information across function boundaries.
They eliminate the "any vs duplication" dilemma.
Every major TS library relies on generics: React, Prisma, Zod, tRPC.
TS 5.x adds const type parameters for literal type inference.

Generic Functions: The Foundation

A generic function declares one or more type parameters using angle brackets. When called, TypeScript infers the type from the arguments, or you can specify it explicitly. The type parameter is then used to enforce relationships between inputs and outputs.

The key insight is that T is not just documentation — it creates a type-level contract. If a function returns T, TypeScript guarantees the output type matches the input type exactly, not some widened version.

Snippet
// Basic generic function — T links input type to output type
function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const num = firstElement([1, 2, 3]);      // type: number
const str = firstElement(['a', 'b', 'c']); // type: string
const obj = firstElement([{ id: 1 }]);     // type: { id: number }

// Multiple type parameters — linking two types
function zip<A, B>(a: A[], b: B[]): [A, B][] {
  return a.map((val, i) => [val, b[i]!]);
}

const pairs = zip([1, 2], ['a', 'b']);
// type: [number, string][]
// value: [[1, 'a'], [2, 'b']]

// Generic arrow function (note the trailing comma in JSX files)
const identity = <T,>(value: T): T => value;
// The trailing comma <T,> prevents JSX from thinking it's a tag

Generic Constraints with extends

Unconstrained generics accept everything, but sometimes you need to guarantee that T has specific properties. The extends keyword constrains a type parameter to types that satisfy a specific shape.

Constraints do not limit T to only that type — they set a minimum requirement. T extends HasLength means T must have at least a length property, but it can have any other properties too. This is structural typing in action.

Snippet
// Constraint: T must have a .length property
interface HasLength {
  length: number;
}

function logWithLength<T extends HasLength>(item: T): T {
  console.log(`Length: ${item.length}`);
  return item;
}

logWithLength("hello");        // ✅ string has .length
logWithLength([1, 2, 3]);      // ✅ array has .length
logWithLength({ length: 10 }); // ✅ object literal matches
logWithLength(42);             // ❌ Error: number has no .length

// keyof constraint — T must be a key of the object
function getProperty<Obj, Key extends keyof Obj>(obj: Obj, key: Key): Obj[Key] {
  return obj[key];
}

const user = { name: 'Ashutosh', age: 28, role: 'Lead Developer' };
const name = getProperty(user, 'name');  // type: string
const age = getProperty(user, 'age');    // type: number
getProperty(user, 'email');              // ❌ Error: "email" is not a key of user

Key Takeaways

extends sets a minimum type requirement, not an exact type.
keyof extracts all property names as a union type.
Obj[Key] is an indexed access type — it returns the type of that property.
This pattern is how libraries type-safely access object properties.

Conditional Types: Type-Level If/Else

Conditional types use the ternary syntax T extends U ? X : Y to create types that change based on a condition. They are the if/else of the type system and enable extremely powerful type transformations.

When used with generics, conditional types distribute over unions. This means if T is a union type A | B, the conditional type checks each member separately: (A extends U ? X : Y) | (B extends U ? X : Y). This distributive behavior is key to understanding utility types like Exclude and Extract.

Snippet
// Basic conditional type
type IsString<T> = T extends string ? 'yes' : 'no';

type A = IsString<string>;  // 'yes'
type B = IsString<number>;  // 'no'
type C = IsString<'hello'>; // 'yes' (string literal extends string)

// Distributive conditional types (distributes over unions)
type ToArray<T> = T extends any ? T[] : never;

type D = ToArray<string | number>;
// Distributes: string[] | number[] (NOT (string | number)[])

// Non-distributive: wrap in tuple to prevent distribution
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type E = ToArrayNonDist<string | number>;
// Does NOT distribute: (string | number)[]

// Practical: Extract only functions from an object type
type FunctionKeys<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];

interface API {
  baseUrl: string;
  timeout: number;
  getUser: (id: string) => Promise<User>;
  deleteUser: (id: string) => Promise<void>;
}

type APIFunctions = FunctionKeys<API>; // "getUser" | "deleteUser"

The infer Keyword: Pattern Matching for Types

The infer keyword lets you "extract" types from inside other types within conditional type expressions. Think of it as destructuring at the type level — it pattern-matches against a type structure and captures inner types.

infer is what makes utility types like ReturnType<T>, Parameters<T>, and Awaited<T> possible. It is arguably the most powerful tool in TypeScript's type system and is essential for advanced library authoring.

Snippet
// Extract return type of a function
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number, y: string) => boolean;
type Result = MyReturnType<Fn>; // boolean

// Extract the type inside a Promise
type UnwrapPromise<T> = T extends Promise<infer Inner> ? Inner : T;

type A = UnwrapPromise<Promise<string>>;           // string
type B = UnwrapPromise<Promise<Promise<number>>>;  // Promise<number>
type C = UnwrapPromise<string>;                     // string (passthrough)

// Deep unwrap (recursive)
type DeepUnwrap<T> = T extends Promise<infer Inner> ? DeepUnwrap<Inner> : T;
type D = DeepUnwrap<Promise<Promise<Promise<boolean>>>>; // boolean

// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Users = ElementOf<User[]>; // User

// Extract first argument type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type LoginArg = FirstArg<(email: string, password: string) => void>; // string

Key Takeaways

infer declares a type variable inside a conditional type's extends clause.
It "captures" whatever type appears at that position in the matched structure.
ReturnType, Parameters, Awaited — all built on infer.
Recursive conditional types (like DeepUnwrap) enable unbounded depth matching.
infer only works inside the extends clause of a conditional type.

Mapped Types: Transform Every Property

Mapped types iterate over the keys of a type and transform each property. They use the [Key in keyof T] syntax — essentially a "for loop" over type properties. Combined with modifiers (+/- readonly, +/- optional), they can make properties required, optional, readonly, or mutable.

TypeScript's built-in utility types — Partial<T>, Required<T>, Readonly<T>, Pick<T,K>, and Record<K,V> — are all mapped types. Understanding how they work lets you create your own domain-specific utility types.

Snippet
// How Partial<T> works internally
type MyPartial<T> = {
  [K in keyof T]?: T[K]; // +? makes each property optional
};

// How Required<T> works internally
type MyRequired<T> = {
  [K in keyof T]-?: T[K]; // -? removes optional modifier
};

// Custom: Make all properties nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface UserConfig {
  theme: string;
  language: string;
  notifications: boolean;
}

type NullableConfig = Nullable<UserConfig>;
// { theme: string | null; language: string | null; notifications: boolean | null }

// Key remapping with `as` (TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }

// Filter properties by type
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type StringProps = OnlyStrings<{ name: string; age: number; email: string }>;
// { name: string; email: string }

Template Literal Types: String Manipulation at the Type Level

Template literal types let you construct and pattern-match string types using backtick syntax — the same template literals you use in runtime code, but at the type level. Combined with mapped types and conditional types, they enable precise string type manipulation.

This feature powers libraries like Prisma (model.$findMany), tRPC (procedure definition), and type-safe routing. It is one of TypeScript's most unique features — no other mainstream type system has string-level type computation.

Snippet
// Basic template literal type
type EventName = `on${"Click" | "Hover" | "Focus"}`;
// "onClick" | "onHover" | "onFocus"

// Cartesian product of unions
type Color = 'red' | 'blue';
type Size = 'sm' | 'lg';
type Variant = `${Color}-${Size}`;
// "red-sm" | "red-lg" | "blue-sm" | "blue-lg"

// Type-safe event system
type EventMap = {
  userCreated: { userId: string; email: string };
  postPublished: { postId: string; title: string };
  orderPlaced: { orderId: string; amount: number };
};

type EventHandler<T extends keyof EventMap> = (payload: EventMap[T]) => void;

function on<T extends keyof EventMap>(event: T, handler: EventHandler<T>) {
  // Implementation...
}

on('userCreated', (payload) => {
  console.log(payload.userId);  // ✅ Type-safe: { userId: string; email: string }
  console.log(payload.amount);  // ❌ Error: 'amount' doesn't exist
});

on('invalidEvent', () => {});   // ❌ Error: not in EventMap

Key Takeaways

Template literal types compute new string types from existing ones.
Unions in templates create cartesian products of all combinations.
Built-in string utilities: Uppercase, Lowercase, Capitalize, Uncapitalize.
Combined with mapped types, they enable type-safe event systems and API clients.

const Type Parameters (TypeScript 5.0+)

The const modifier on type parameters is a TypeScript 5.0+ feature that tells the compiler to infer the narrowest possible type — literal types, readonly tuples, and readonly object types. Without const, TypeScript widens literals to their base types.

This is critical for configuration objects, route definitions, and any API where you want to preserve exact literal values at the type level. Before TS 5.0, you had to use "as const" at every call site, which was easy to forget.

Snippet
// Without const: types are widened
function createRoutes<T extends Record<string, string>>(routes: T): T {
  return routes;
}
const routes1 = createRoutes({ home: '/', blog: '/blog' });
// type: { home: string; blog: string } — literals are lost!

// With const: literal types are preserved
function createRoutes<const T extends Record<string, string>>(routes: T): T {
  return routes;
}
const routes2 = createRoutes({ home: '/', blog: '/blog' });
// type: { readonly home: "/"; readonly blog: "/blog" } — exact literals!

// Real-world: Type-safe config builder
function defineConfig<const T extends {
  port: number;
  env: 'development' | 'staging' | 'production';
  features: string[];
}>(config: T): T {
  return config;
}

const config = defineConfig({
  port: 3000,
  env: 'production',
  features: ['auth', 'analytics', 'blog'],
});
// config.env is exactly 'production', not the union type
// config.features is readonly ['auth', 'analytics', 'blog']

Real-World Pattern: Type-Safe API Client

This pattern combines generics, mapped types, conditional types, and template literals to create a fully type-safe API client. The types guarantee that request bodies, URL parameters, and responses all match your API schema at compile time.

This is the same pattern used by libraries like tRPC, Hono, and Elysia. Understanding it makes you capable of building (or contributing to) professional TypeScript libraries.

Snippet
// Define your API schema
interface APISchema {
  'GET /users': { response: User[]; query: { page: number; limit: number } };
  'GET /users/:id': { response: User; params: { id: string } };
  'POST /users': { response: User; body: CreateUserInput };
  'PUT /users/:id': { response: User; params: { id: string }; body: UpdateUserInput };
  'DELETE /users/:id': { response: void; params: { id: string } };
}

// Extract method and path
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Routes = keyof APISchema;

// Type-safe client
type RequestConfig<T extends Routes> = Omit<APISchema[T], 'response'>;
type ResponseOf<T extends Routes> = APISchema[T]['response'];

async function apiClient<T extends Routes>(
  route: T,
  config: RequestConfig<T>
): Promise<ResponseOf<T>> {
  // Implementation: parse route, substitute params, make fetch call
  const [method, path] = (route as string).split(' ');
  const response = await fetch(path, { method });
  return response.json();
}

// Usage — fully type-safe!
const users = await apiClient('GET /users', { query: { page: 1, limit: 10 } });
// users: User[]

const user = await apiClient('GET /users/:id', { params: { id: '123' } });
// user: User

await apiClient('POST /users', { body: { name: 'Ashutosh', email: 'a@ih.com' } });
// body is typed as CreateUserInput — compile error if wrong shape

Key Takeaways

The API schema is a single source of truth for all route types.
RequestConfig and ResponseOf extract the right types for each route.
Compile-time errors if you pass wrong body, params, or query shape.
This pattern eliminates runtime type errors in API communication.

Top Interview Questions on TypeScript Generics

Q1: What problem do generics solve? → They enable reusable, type-safe code without "any" or duplication.

Q2: What is the difference between <T> and <T extends X>? → Unconstrained T accepts everything. T extends X requires T to satisfy shape X.

Q3: Explain conditional types → T extends U ? X : Y — type-level if/else. Distributes over unions by default.

Q4: What does infer do? → Captures a type inside a conditional type. Used to build ReturnType, Parameters, and Awaited.

Q5: How do mapped types work? → [K in keyof T] iterates over all keys of T, letting you transform each property.

Q6: What are template literal types? → Backtick types that compute new string types. Enable type-safe event names, routes, etc.

Q7: What is keyof? → Extracts all property names of a type as a union. keyof { a: 1; b: 2 } = "a" | "b".

Q8: Explain the const modifier in type parameters → Forces narrowest inference (literal types). Eliminates need for "as const" at call sites.

Q9: How does Partial<T> work internally? → Mapped type: { [K in keyof T]?: T[K] } — adds optional modifier to all properties.

Q10: What is a higher-kinded type, and does TS support it? → A type that takes another type as a parameter (like Functor). TS does not support true HKTs, but you can simulate them with generic interfaces.

Key Takeaways

TypeScript generics are not just a feature — they are the foundation of the type system. Every type-safe library, every React hook type, every API client is built on generics. Mastering them is the difference between a TypeScript user and a TypeScript expert.

The progression is: generic functions → constraints → conditional types → infer → mapped types → template literals → const type parameters. Each layer builds on the previous one, and together they enable compile-time guarantees that catch entire categories of bugs.

In 2026, with TypeScript 5.x, the type system is mature enough to express almost any type relationship. The question is no longer "can TypeScript type this?" but "how do I type this correctlyness?"

Key Takeaways

Generics = type variables that preserve type information.
extends = constraints. keyof = property name extraction. infer = pattern matching.
Conditional types = type-level if/else. Distribute over unions by default.
Mapped types = "for loop" over type properties. Key remapping with `as`.
Template literal types = string computation at the type level.
const type params (TS 5.0+) = infer exact literals, not widened types.
Practice: build utility types from scratch for interview readiness.
AS
Article Author
Ashutosh
Lead Developer

Related Knowledge

Tutorial

React 19 Hooks Mastery

5m read
Tutorial

Next.js 15 — The Complete Guide

5m read
Tutorial

JavaScript Fundamentals

5m read
Article

Understanding Closures in JavaScript: The Complete 2026 Guide

22 min read
Article

React 19 Server Components: The Definitive 2026 Guide

28 min read
Article

Next.js 15 App Router Masterclass: Everything You Need to Know

25 min read