Skip to Content
Course content

109: Constraining Generics with keyof

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR from a developer who was building a dynamic settings dashboard. He had written a utility function to pull specific configuration values out of a large settings object. Initially, his function looked something like this: function getConfigValue<T>(settings: T, key: string) { return settings[key]; }. He was baffled when TypeScript started screaming at him, claiming that an index signature was missing. Even worse, when he forced it to work using any, he realized he had no autocomplete for the keys, and a single typo in a string literal was causing silent runtime failures.

The problem was that he had told TypeScript that settings was some type T, but he told it that key was any string in the universe. TypeScript couldn't guarantee that the string passed as the key actually existed on the object. To fix this, we need to create a formal relationship between the object's type and the key's type.

Linking Keys to Types with keyof

The keyof operator is a powerful tool that takes an object type and produces a union of its keys. If you have a User interface with id, email, and username, keyof User literally evaluates to "id" | "email" | "username". It's like telling TypeScript, "I only care about the labels on this specific box."

When we combine this with generics, we can use a "constraint." Instead of just using T, we introduce a second generic, K, and tell TypeScript that K must be a subset of the keys of T. Here is how that looks in practice:

interface AppConfig {
  apiUrl: string;
  timeout: number;
  retries: number;
}

function getSetting<T, K extends keyof T>(config: T, key: K) {
  return config[key];
}

const myConfig: AppConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3
};

// This works perfectly and provides autocomplete for 'apiUrl', 'timeout', etc.
const url = getSetting(myConfig, "apiUrl"); 

// This will trigger a compile-time error because "version" isn't a key of AppConfig
const version = getSetting(myConfig, "version"); 

The Magic of Type Inference

One of the best parts about this pattern is that you rarely have to explicitly pass the types when calling the function. Because you're passing myConfig as the first argument, TypeScript infers that T is AppConfig. Once T is locked in, it knows that K must be one of the keys of AppConfig. This creates a tight loop of type safety where the second argument is constrained by the first.

I often see developers try to avoid this by using Record<string, any>, but that's essentially giving up. By using K extends keyof T, you aren't just stopping errors; you're documenting the code. Anyone using your function will see exactly which keys are valid just by hovering over the argument in their IDE.

Handling Value Types

You'll notice that the return type of the function above is automatically inferred as the specific type of the property you accessed. If you pass "timeout", the return type is number; if you pass "apiUrl", it's string. This is the real power of constraining generics. We aren't just validating the input; we are preserving the type information all the way through to the output, avoiding the dreaded any cast that plagues so many large-scale TypeScript projects.




📋 Practical Task

Exercise: Building a Type-Safe Event Emitter Lookup

You are building a small Event Emitter system. You have an Events interface that defines the payload for different event types. Your goal is to create a function called getEventPayload that takes the Events object and a specific event name, returning the correctly typed payload.

Complete the code below by adding the necessary generic constraints to the getEventPayload function so that it only accepts valid event names and returns the correct value type.

interface Events {
  userLoggedIn: { userId: string; timestamp: number };
  orderPlaced: { orderId: string; amount: number };
  connectionLost: { reason: string };
}

const eventStore: Events = {
  userLoggedIn: { userId: "u123", timestamp: 1625097600 },
  orderPlaced: { orderId: "o456", amount: 99.99 },
  connectionLost: { reason: "Timeout" },
};

// TODO: Fix the generic signature of this function
function getEventPayload<T, K extends keyof T>(events: T, eventName: K) {
  return events[eventName];
}

// Test cases:
const loginData = getEventPayload(eventStore, "userLoggedIn"); // Should be inferred as { userId: string; timestamp: number }
const orderData = getEventPayload(eventStore, "orderPlaced");   // Should be inferred as { orderId: string; amount: number }

// This should throw a TypeScript error:
// const invalidData = getEventPayload(eventStore, "somethingRandom"); 
Rating
0 0

There are no comments for now.

to be the first to leave a comment.