Universal identity type function

As per nowadays trends, TypeScript is a smart language which can understand types of static code without explicit indication of type thanks to its type inference ability.

Type inference is really one of the best thing about this language, I use it every day I write a code. And strongly recommend you to understand where and why TypeScript can infer types and where cannot - so you have to explicitly mark appropriate type in this case.

Problem to solve

Need a way to enforce object literal structure while inferring exact properties of object as literals instead of general types "string" or "number".

Solution

Universal identity function - a function that returns exactly what it receives -

// src/utils/type.util.ts
export const inferIdentity = <BaseType,>() => <T extends BaseType>(value: T): T => value;

Usage demo:

interface CustomRoute {
    path: string;
}

const homeRoute = inferIdentity<CustomRoute>()({
    path: '/',
} as const);

const otherRoute = inferIdentity<CustomRoute>()({
    pathTypo: '/other',
//  ^ Error: Object literal may only specify known properties, and 'pathTypo' does not exist in type 'CustomRoute'.(2345)
} as const);

If some type is widely used, you can reuse identity easily:

export const identityRoute = inferIdentity<CustomRoute>();

// reuse across the app
const anotherRoute = identityRoute({
    path: '/another',
} as const);

Note, we name new util as identityRoute instead of inferIdentityRoute because function is identity function now:

  • identityRoute is not a high order function anymore, but a simple function.
  • inferIdentity returns identity function instead of its argument, like identity function would do.

How it works?

Because of a type closure. Similar to runtime JavaScript closure, we can capture generic type for its child function.

Therefore, actually, the "universal identity function" is not an identity function but a high order function that returns an identity function and type closure helps to constraint generic type needed.

Why it is useful?

To explain reasoning here, let's start with a simple scenario.

Imagine you need an object literal config to use across the app:

export const config = {
  timeout: 1e3,
}

// and the related type is
const config: {
    timeout: number;
}
config literal with a one second timeout

Now, we want to use our great type inference in some places to understand exact timeout value defined. Currently timeout is any number , so we add as const to keep exact values of properties:

const config = {
  timeout: 1e3,
} as const;

// and the related type is
const config: {
    readonly timeout: 1000;
}

Cool, now we can infer types of whole config, or its properties and further transform them via mapped types and constraint code with specific union literals, for instance.

Such code is actually enough for a small demo app, but in reality production apps:

  • Have much bigger object literals
  • Hard to remember which exact properties are available for every object literal
  • Need a reliable way (compile error) to avoid typos in object properties

For that, interfaces and type aliases comes to the rescue:

const config: IConfig = {
  timeout: 1e3,
}

Which is great - TypeScript now suggests us which properties are available for us to configure, and will raise a compile error in case of typo or wrong type defined for a property.

Only one thing left for it to be perfect - we need to know exact values of object literals instead of abstract IConfig interface.

For that, you come up with an idea of identity function:

const configIdentity = <T extends { timeout: number }>(config: T): T => config;

const config = configIdentity({
    timeout: 1e3,
} as const);

// and the related type is
const config: {
    readonly timeout: 1000;
}

Awesome, now you can create all the identity functions for all the object literals and even primitive types to make your codebase constrained and obvious.

Or you may use a single universal identity function shown at solution section of this article πŸ˜‰.