🔷 TypeScript Q89 / 101

Explain keyof with generics.

AI-Powered Answer ✓ Answered

TypeScript's `keyof` operator and generics are powerful features that enhance type safety and code reusability. When combined, they allow you to write functions and classes that operate on object properties in a type-safe manner, ensuring that you only access properties that actually exist on a given object type. This explanation will detail `keyof`, generics, and demonstrate their combined utility.

What is `keyof`?

The keyof operator takes an object type and produces a string or string literal union of its known, enumerable property names. It's incredibly useful for getting the exact keys of a type.

typescript
type User = {
  id: number;
  name: string;
  email: string;
};

type UserKeys = keyof User; // 'id' | 'name' | 'email'

const user: User = { id: 1, name: 'Alice', email: 'alice@example.com' };

function printKey(key: UserKeys) {
  console.log(key);
}

printKey('name'); // Valid
// printKey('address'); // Error: Argument of type '"address"' is not assignable to parameter of type 'UserKeys'.

What are Generics?

Generics allow you to write components that work with a variety of types instead of a single one. This makes your code more flexible and reusable, while still providing compile-time type checking. They act as "type variables" for functions, classes, or interfaces.

typescript
function identity<T>(arg: T): T {
  return arg;
}

let output1 = identity<string>("myString"); // type of output1 is string
let output2 = identity<number>(100);    // type of output2 is number

`keyof` with Generics: Combining Powers

The real power emerges when keyof is used with generics. This combination allows you to write functions that are generic over an object type T and also generic over a property key K, where K is constrained to be one of the keys of T. This creates highly type-safe and flexible functions for object manipulation.

Practical Example: `getProperty` Function

A common pattern is to create a generic getProperty function that can extract a property from any object, ensuring that the property name provided actually exists on the object.

typescript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

interface Person {
  name: string;
  age: number;
  city: string;
}

const person: Person = {
  name: "John Doe",
  age: 30,
  city: "New York"
};

const personName = getProperty(person, "name"); // type is string, value is "John Doe"
const personAge = getProperty(person, "age");   // type is number, value is 30

// getProperty(person, "country"); // Error: Argument of type '"country"' is not assignable to parameter of type '"name" | "age" | "city"'.

Why is this useful?

  • Type Safety: Prevents accessing non-existent properties at compile time, reducing runtime errors.
  • Refactoring Safety: If you rename a property in Person, TypeScript will flag errors in calls to getProperty using the old name.
  • Code Reusability: The getProperty function can work with any object type that matches the generic constraint.
  • Intellisense/Autocompletion: IDEs can provide suggestions for valid keys based on the object type.

Constraints and `extends` keyword

The K extends keyof T syntax is crucial here. It tells TypeScript that the generic type K must be a subtype of keyof T. This means K must be one of the literal string types that represent the keys of T. This constraint is what enables the type safety.

typescript
// T represents the object type, K represents the key type
function printKeyValue<T, K extends keyof T>(obj: T, key: K) {
  console.log(`Key: ${String(key)}, Value: ${obj[key]}`);
}

type Car = { make: string; model: string; year: number };
const myCar: Car = { make: "Toyota", model: "Camry", year: 2020 };

printKeyValue(myCar, "make"); // Valid: K is "make", which extends keyof Car ("make" | "model" | "year")
printKeyValue(myCar, "year"); // Valid: K is "year"

// printKeyValue(myCar, "color"); // Error: Argument of type '"color"' is not assignable to parameter of type '"make" | "model" | "year"'.

Conclusion

By combining keyof with generics, TypeScript empowers developers to write highly robust, flexible, and type-safe code for interacting with object properties. This pattern is fundamental in advanced TypeScript for building libraries, utility functions, and enforcing strict data access policies, ultimately leading to more reliable applications.