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