29
TIL: get strongly typed HTTP headers with TypeScript
I’m a developer of a backend framework. It is written in TypeScript. I want to:
http.IncomingMessage
) from my usershttp.IncomingHttpHeaders
).Turns out all of that is possible.
Consider
http.IncomingHttpHeaders
interface: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:
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: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.
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:
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:
type StandardHeader = keyof StandardHeaders;
That’s what VSCode thinks about
StandardHeader
:
Nice type literal with only known headers. Let’s plug it into
getHeader(name: StandardHeader)
and try to use it:
Auto-completion works and compilation breaks if we type something wrong there:

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: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:
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:

And also it includes them into compile-time check:

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 =).
29