TypeScript's keyof operator and possible uses (Refreshed post, June 2023)
Tags: typescript, programming, tutorial
Published: 2023-06-27 13:35:00

In this article, I'll take you on a journey through the world of TypeScript's `keyof` operator and its uses.

## Type-safe function parameters

`keyof` is a nifty operator that lets you dive into the heart of an object type, extracting and manipulating its keys with ease. Imagine dynamically accessing and working with specific properties, performing operations, and crafting utility functions tailored the types properties for added type safety. Say goodbye to boilerplate code for each type and embrace the freedom to create a more generic and reusable solution.

```typescript
type Person = {
  name: string;
  age: number;
  email: string;
};

function getProperty(obj: Person, key: keyof Person) {
  return obj[key];
}

const person: Person = {
  name: "John",
  age: 91,
  email: "john@example.com",
};

// Below we access the `name` and `age` properties
// programmatically from the keys of the type definition
const name = getProperty(person, "name");
const age = getProperty(person, "age");

console.log(name); // Expected output: "John"
console.log(age); // Expected output: 91

```

The above runs and typechecks just fine but let's try to pass in the name of a property that does not exist on the Person type:

```typescript
const emailAddress = getProperty(person, "emailAddress");
// Expected type error:
// Argument of type '"emailAddress"' is not assignable
// to parameter of type 'keyof Person'.
```
Of course, we return to the well with the correct property to retrieve and able to move past the type error:

```typescript

const email = getProperty(person, "email");
console.log(email) // Expected output: "john@example.com"
```

## Type-based return types

TypeScript's `keyof` operator can unlock the secret to type property-based return types.

By combining it with indexed access types (`[]`), you can create functions that strictly accept or return values based on the keys of an object. No more accidental typos or invalid arguments slipping through the cracks! Whether you're dealing with dynamic configurations or APIs that demand specific property names, keyof has got your back.

```typescript
type Configuration = {
  apiKey: string;
  apiUrl: string;
  timeout: number;
};

const config: Configuration = {
  apiKey: "abc123",
  apiUrl: "https://example.com/api",
  timeout: 5000,
};

function getConfiguration<T extends keyof Configuration>(key: T): Configuration[T] {
  return config[key];
}

const apiKey: string = getConfiguration("apiKey"); // Type-safe parameter usage
console.log(apiKey); // Output: abc123

const timeout: number = getConfiguration("timeout"); // Type-safe return type
console.log(timeout); // Output: 5000
```

## Key/Type Paired arguments

Brace yourself for an exhilarating journey where static analysis takes the stage. With keyof, the compiler becomes your vigilant guardian, meticulously validating your code to catch any mishaps related to property names and other parameters derived from the prpoerty name. Imagine catching those errors early in the development process, paving the way for rock-solid combinator libraries.

Assume we define a Product type with id, name, and price properties with different associated types.

```typescript
enum Currency {
  USD,
  EUR,
  JPY,
};

type CurrencyAmount = {
  currency: Currency;
  amount: number;
};

type Product = {
  id: number;
  name: string;
  price: CurrencyAmount;
};

function updateProduct(
    product: Product,
    key: keyof Product,
    value: Product[keyof Product]
  )
{
  return { ...product, key: value };
}
const price: CurrencyAmount = {
  currency: Currency.USD,
  amount: 10
};
const product: Product = {
  id: 1,
  name: "Premium",
  price,
};

```
Dissecting the TypeScript function `updateProduct`:

- The function `updateProduct` is declared with the `const` keyword. It takes three parameters: `product`, `key`, and `value`.

- The product parameter is of type `Product`, which suggests that it expects an object of type `Product` as an argument.

- The `key` parameter is of type `keyof Product`. Here, `keyof Product` represents a union type consisting of all the keys (property names) of the `Product` type. It means that key can only accept a valid key from the `Product` object.

- The value parameter is of type `Product[keyof Product]`. It represents the type of the value corresponding to the key in the `Product` object. This ensures that the value parameter matches the type of the property indicated by the key parameter.

- The return type of the `updateProduct` function is `Product`, indicating that it will return an object of type `Product`.

- Inside the function body, a new object is created using the spread operator (`...product`) to copy all the properties of the `product` parameter.

- The `key` parameter is used as a property name inside square brackets (`[key]`) to dynamically access the property in the new object.

- The value parameter is assigned to the property indicated by the `key`, effectively updating the corresponding property value.

- The updated object is returned as the result of the function.

In short, the `updateProduct` function takes an object of type Product, a valid property name from Product, and a value matching the type of the indicated property. It will create an object with the updated property value and returns it.

Let's try using the `updateProduct` to change valid properties with values that have types in the property types of Product:

```typescript
// Below updates the property dynamically restricted to just
// the type of the keys in the type
const returnedProd1 =
  updateProduct(product, "name", "Professional");
console.log(returnedProd1.name); // Output: "Professional"

// Yo, inflation is still a thing so increase the price
const newPrice: CurrencyAmount = {
  currency: Currency.USD,
  amount: 12,
};
const returnedProd2 =
  updateProduct(product, "price", newPrice);
console.log(returnedProd2.price); // Output: {
//  "currency": 0,
//  "amount": 12
//}
```
Above you'll notice we get the expected behavior where the values of the property update as expected. Yet we all know the happy path is not the most interesting, so what happens when we try to update the product value altering a property that doesn't exist in the Product type?

```typescript
// Failure mode where key given doesn't match underlying type
updateProduct(product, "quantity", 5);
// Type error: Property 'quantity' does not exist in type
// 'keyof Product'
```
A type error. Music to my ears.

Now we will try the case where the value parameter given doesn't match the type of any of the underlying type's property types:

```typescript
// Failure mode where value doesn't match underlying
// property type
updateProduct(product, "id", false);
// Type error: Argument of type 'boolean' is not assignable
// to parameter of type 'string | number | CurrencyAmount'.
```
Perfect another type error.

## A use case I'm Partial<T> to!

However, what if we try to update the id property of the `product` value to a string?

```typescript
const returnedProd3 = updateProduct(product, "id", "hello world");
console.log(returnedProd3.id); // Expected output: 1
console.debug(returnedProd3); // Expected output: {
//  "id": 1,
//  "name": "Professional",
//  "price": {
//    "currency": 0,
//    "amount": 12
//  }
//}
```
This is not ideal since it doesn't result in a type error. However, the behavior retains some weak notion of order by ignoring the invalid update to the id property with a string value instead of its expected type number.

Of course, if you are familiar with the `Partial<T>` type you will know that it offers a more ergonomic way for application developers to provide a partial object with well-typed key-value pairs corresponding to the `T` type for full coverage. Let's redefine the `updateProduct` function as follows:

```typescript
const updateProduct2 = (
    product: Product,
    updates: Partial<Product>,
  ): Product => ({ ...product, ...updates });

```
Using the new `updateProduct` function definition that leverages `Partial<Product>` offers consumers improved developer ergonomics in multiple ways by:

- accepting multiple property values to update in one operation
- improving type safety

```typescript
const returnedProd4 = updateProduct2(product, {
  name: "Professional",
  price: newPrice,
});
console.debug(returnedProd4); // Expected output: {
//  "id": 1,
//  "name": "Professional",
//  "price": {
//    "currency": 0,
//    "amount": 12
//  }
//}
console.debug(product); // Expected output: {
//  "id": 1,
//  "name": "Premium",
//  "price": {
//    "currency": 0,
//    "amount": 10
//  }
//}
```
Above you see that we passed in two properties to be updated in the returned object.

Now we will kick the tires on the type-safety of the new definition:

```typescript
const returnedProd5 = updateProduct2(product, {
  id: "foobar",
});
console.debug(returnedProd5);
// Type error: Type 'string' is not assignable
// to type 'number'.
```
Now we get a type error. Success!

Under the hook, `Partial<T>` is still using the `keyof` indexed type query operator using something like the following definition:

```
type Partial<T> = {
  [P in keyof T]?: T[P];
};
```
For people newer to TypeScript I'll break down the type definition above:

- The type `Partial<T>` is defined. It represents a new type that takes a generic parameter `T`, which can be any object type.
- Inside the curly braces `{}`, we have a key-value pair syntax that defines the structure of the `Partial<T>` type.
- `[P in keyof T]` is a mapped type that iterates over each key `P` in the keys of the `T` type. `keyof T` is a utility type in TypeScript that retrieves all the keys of the type `T`.

The `?` postfix modifier indicates that the property is optional. It means that each property of the resulting type `Partial<T>` may or may not exist in the original type `T`.

`T[P]` is the type of the property with the key `P` in the original type `T`. It ensures that the value assigned to each property in `Partial<T>` is of the same type as the corresponding property in `T`. This is the key reason why we get better type-safety with the definition of `updateProduct2` function.

By using this type definition, the `Partial<T>` type allows you to create a new type that shares the same keys as the original type `T`, but with all properties marked as optional. This is useful when you want to express that some properties of an object can be assigned undefined or omitted altogether.

With `Partial<T>`, you hold the key to create a brand new type that mirrors the original, but with the freedom to omit or assign undefined to any property. It's like having a personalized toolkit for crafting the perfect object. Say goodbye to rigid structures and embrace the art of possibility.

## Update
- June 2023
: Coming soon I will walk through step by step how to use common types in TypeScript that leverage keyof.