Implementing a Custom `Includes` Utility Type in TypeScript
Tags: typescript
Published: 2023-12-15 19:57:16


# Introduction 

TypeScript offers a powerful type system that allows developers to create complex type-safe applications. One of the more advanced techniques in TypeScript is creating utility types, which can enhance type safety and code readability. Today, we're going to dive into creating a custom `Includes` utility type, exploring several key TypeScript concepts along the way.

## What is the `Includes` Utility Type?

The `Includes` utility type checks if a given type is included in a tuple or an array type. It's similar in concept to JavaScript's array `.includes()` method, but for types. Implementing `Includes` in TypeScript is an excellent way to understand some of the more nuanced features of the language.

## Key TypeScript Concepts

Before we start, let's discuss some TypeScript concepts that are crucial for understanding our implementation:

### Conditional Types

Conditional types in TypeScript allow you to define a type that can have different forms based on some condition. It's like an `if` statement, but for types.

```typescript
type Check<T> = T extends string ? 'String' : 'Not String';
```

### Infer Keyword

The `infer` keyword is used within conditional type branches to infer types within other types. It's often used in tuple and function types.

```typescript
type ElementType<T> = T extends (infer U)[] ? U : never;
```

### Recursive Types

Recursive types are types that reference themselves in their definition. They're useful for defining types that need to work through a structure of unknown depth, like linked lists or tree structures.

```typescript
type RecursiveArray<T> = Array<T | RecursiveArray<T>>;
```

### Strict Type Comparison

TypeScript's structural type system is quite permissive. This means that TypeScript focuses on the shape that values have. For instance, if two different interfaces have the same shape (the same properties with the same types), TypeScript will consider them to be compatible, even if they were declared separately.

```typescript
interface Person {
  name: string;
}

interface User {
  name: string;
}

let person: Person;
let user: User;

// TypeScript considers these types compatible due to structural typing.
person = user;
user = person;
```

#### Need for Strict Type Comparison

There are scenarios where you need to distinguish types more strictly, beyond just their structure. For example, you might want to ensure that two types are exactly the same and not just structurally compatible. This is where strict type comparison comes into play.

#### Implementing Strict Type Comparison

To implement strict type comparison, you can use a clever combination of conditional types and the `infer` keyword. The `Equal` type uses a higher-order function technique to compare two types.

```typescript
type Equal<X, Y> = 
  (<T>() => T extends X ? 1 : 2) extends 
  (<T>() => T extends Y ? 1 : 2) ? true : false;
```

##### How it Works:

1. **Function Type Comparison:** The type `Equal` creates two function types. Each function returns either `1` or `2` based on a conditional type check.

2. **Conditional Types:** `T extends X ? 1 : 2` checks if a hypothetical type `T` would extend type `X`. If it does, the function returns `1`, otherwise `2`. The same check is done for `Y`.

3. **Extends Check for Functions:** The key part is comparing these two function types. If `X` and `Y` are exactly the same, any type `T` will extend both `X` and `Y` in the same way, making the two function types identical. Therefore, `(<T>() => T extends X ? 1 : 2)` will be considered equivalent to `(<T>() => T extends Y ? 1 : 2)`.

4. **Resulting in True or False:** If the function types are equivalent, it means `X` and `Y` are exactly the same (strictly equal), so the whole type evaluates to `true`. Otherwise, it evaluates to `false`.

##### Example:

```typescript
type IsStringEqualToString = Equal<string, string>; // true
type IsStringEqualToNumber = Equal<string, number>; // false
```

Here, `IsStringEqualToString` evaluates to `true` because `string` is strictly equal to `string`. Conversely, `IsStringEqualToNumber` evaluates to `false` because `string` is not strictly equal to `number`.

## Implementing `Includes`

Now, let's combine these concepts to create our `Includes` utility type:

```typescript
type Includes<T extends readonly any[], U> = T extends [infer First, ...infer Rest]
  ? Equal<First, U> extends true
    ? true
    : Includes<Rest, U>
  : false;
```

Here's what's happening:

- We use conditional types to check each element of the tuple.
- `T extends [infer First, ...infer Rest]` splits the tuple into its first element and the rest. This is a form of tuple destructuring using the `infer` keyword.
- We use a custom `Equal` type to strictly compare `First` with `U`. If they're equal, we return `true`.
- If not, we recursively call `Includes` with the rest of the tuple (`Rest`).

## Testing Our `Includes` Type

To ensure our utility type works correctly, let's test it with a few examples:

```typescript
type Test1 = Includes<['a', 'b', 'c'], 'a'>; // true
type Test2 = Includes<['a', 'b', 'c'], 'd'>; // false
type Test3 = Includes<[1, 2, 3], 2>;          // true
type Test4 = Includes<[1, 2, 3], 4>;          // false
```

## Conclusion

Creating custom utility types like `Includes` is a great way to dive deeper into TypeScript's type system. It helps you understand and leverage advanced concepts such as conditional types, recursive types, and strict type comparisons. Not only does this enhance your TypeScript skills, but it also leads to more robust and maintainable code.

TypeScript's type system is deep and powerful, and mastering it can greatly improve the quality of your code. Happy typing!