TIL: get strongly typed HTTP headers with TypeScript
Tags: todayilearned, typescript, node, webdev
Published: 2021-12-20 07:12:52

## Goal

I’m a developer of a backend framework. It is written in TypeScript. I want to:
- Hide the actual request object (`http.IncomingMessage`) from my users
- Yet provide my users with access to HTTP headers on the request (`http.IncomingHttpHeaders`).
- Provide IntelliSense (auto-completion) so it is easier to find headers people want to use.
- Provide **compile-time** checking that there’s no type in a header.
- Do not limit my users as to which headers they can use, so the list of headers must be extensible from their services.

Turns out all of that is possible.

## Implementation

Consider `http.IncomingHttpHeaders` interface:

```typescript
interface IncomingHttpHeaders {
    'accept-patch'?: string;
    'accept-ranges'?: string;
    'accept'?: string;
    …
    'warning'?: string;
    'www-authenticate'?: string;
    [header: string]: string | string[] | undefined;
}
```
The problem with it that while it does have header names hardcoded it:
- does not provide a way to extend that list.
- provides index signature, which means all type safety goes out the window.

So in order to conceal the actual request from my users, I’ve got a class called `Context` and I hand out instances of that to handlers for each request:

```typescript
export class Context {
    constructor(private req: http.IncomingMessage) { }
    …
    getHeader(name: ?) {
        return req.headers[name];
    }
}
…
```

What we want to do is to introduce some kind of type instead of `?` so that it allows **only** those headers from `http.IncomingHttpHeaders` that are hard-coded, we will call them “known keys”.

We also want our users to be able to extend this list easily.

### Problem 1

Can’t use simple `type StandardHeaders = keyof http.IncomingHtppHeaders` because the interface has index signature, that resolves into `StandardHeaders` accepting anything so auto-completion and compile-time checking doesn’t work.

Solution - remove index signature from the interface. TypeScript 4.1 and newer allows key re-mapping and TypeScript 2.8 and newer has Conditional Types. We only provide 4.1 version here:

```typescript

type StandardHeaders = {
    // copy every declared property from http.IncomingHttpHeaders
    // but remove index signatures
    [K in keyof http.IncomingHttpHeaders as string extends K
        ? never
        : number extends K
        ? never
        : K]: http.IncomingHttpHeaders[K];
};

```
That gives us copy of `http.IncomingHttpHeaders` with index signatures removed.

It is based on the fact that `‘a’ extends string` is `true` but `string extends ’a’` is `false`. Same for `number`.

Now we can just:
```typescript
type StandardHeader = keyof StandardHeaders;
```
That’s what VSCode thinks about `StandardHeader`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jiu295j4g61tjdf3f0c9.png)

Nice type literal with only known headers. Let’s plug it into `getHeader(name: StandardHeader)` and try to use it:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t0a23k3p8tg3mto03p0o.png)

Auto-completion works and compilation breaks if we type something wrong there:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h3r5rpvh95lymtdvj78b.png)

### Problem 2.

We’re a framework, this set of headers is pretty narrow, so we need to give people ability to extend it.

This one is easier to solve that the previous one. Let’s make our `Context` generic and add several things:
- limit generic to **string** type literals
- provide a sensible default

```typescript
export class Context<TCustomHeader extends string = StandardHeader> {
    constructor(private req: http.IncomingMessage) { }
    …
    getHeader(name: StandardHeader | TCustomHeader) {
        return req.headers[name];
    }
    …
}
```
Ok, now our users can write something like this:

```typescript
const ctx = new Context<'X-Foo' | 'X-Bar'>(...);
const foo = ctx.getHeader('X-Foo');
const bar = ctx.getHeader('X-Bar');
```

And it will auto-complete those headers:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5v5woyngc0vlpj34wv8d.png)

And also it includes them into compile-time check:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dhut8jcwrfnltk374di5.png)

## Further improvements

Because we’re a framework, users won’t be creating instances of `Context` class themselves, we’re handing those out. So instead we should introduce a class `ContextHeaders` and replace `getHeader(header: StandardHeader)` with generic method `headers< TCustomHeader extends string = StandardHeader>: ContextHeaders<StandardHeader | TCustomHeader>`

That is left as exercise for reader =).