· Jake Worth

TypeScript generics via the identity function


Generics let you write functions that work for many types without giving up type safety. The clearest way to see them is a concept from functional programming: identity.

Identity is a unary function (one argument) that returns its argument.1 The primary use case is filtering items that don’t pass a test of truthiness.

filter.js
const identity = (arg) => arg;
['', false, 'keep', null, undefined, 'these'].filter(identity);
// ['keep', 'these'];

This works in JavaScript, but TypeScript won’t let us be so liberal. This same code in a .ts file returns the following error:

error TS7006: Parameter 'arg' implicitly has an 'any' type.

any is an anti-pattern, so lets add a union type to the argument:

filter.ts
type Arg = string | number | boolean | null;
const identity = (arg: Arg) => arg;

It works, but its ugly. And what if we forget a valid input type (notice, I forgot undefined), or TypeScript introduces a new primitive that arg could be? It will break.

And we aren’t typing the return. Let’s do that. We’ll call our new input/output type “T”, for “Type”2:

filter.ts
type T = string | number | boolean | null | undefined;
const identity = (arg: T): T => arg;

Those familiar with the generic syntax will know that we’ve essentially written one. Here’s that implementation.

filter.ts
const identity = <T>(arg: T): T => arg;

This is our way of saying “Whatever type this function receives, it returns that type.”

In case one thought the union and generic were both good, here are those implementations side-by-side and the type declarations emitted by them:

filter.ts
const genericIdentity = <T>(arg: T): T => arg;
const genericResult = genericIdentity(42);
type Arg = string | number | boolean | null | undefined;
const unionIdentity = (arg: Arg) => arg;
const unionResult = unionIdentity(42);
filter.d.ts
declare const genericIdentity: <T>(arg: T) => T;
declare const genericResult = 42;
type Arg = string | number | boolean | null | undefined;
declare const unionIdentity: (arg: Arg) => Arg;
declare const unionResult: Arg;

These two lines show why generics win.

filter.d.ts
declare const genericResult = 42; // Type-safe! 😎
declare const unionResult: Arg; // Type-safe?! 😥

We want the stricter type whenever we can get it.

That does not mean every function should be generic. If the input and output types are fixed and unrelated, for instance, string in and number out, a concrete signature is clearer:

const length = (arg: string): number => arg.length;

Footnotes

  1. I’m not the first to use identity to explain generics. It’s a classic for a reason.

  2. More on this convention: Answer: In TypeScript, what does <T> mean?