A collection of useful TypeScript tricks
Deep immutable (readonly) generic type for specifying multi-level data structures that cannot be modified.
Example:
let deepX: DeepImmutable<{y: {a: number}}> = {y: {a: 1}};
deepX.y.a = 2; // Fails as expected!
Credit: @nieltg in Microsoft/TypeScript#13923 (comment)
type Primitive = undefined | null | boolean | string | number | Function
type Immutable<T> =
T extends Primitive ? T :
T extends Array<infer U> ? ReadonlyArray<U> :
T extends Map<infer K, infer V> ? ReadonlyMap<K, V> : Readonly<T>
type DeepImmutable<T> =
T extends Primitive ? T :
T extends Array<infer U> ? DeepImmutableArray<U> :
T extends Map<infer K, infer V> ? DeepImmutableMap<K, V> : DeepImmutableObject<T>
interface DeepImmutableArray<T> extends ReadonlyArray<DeepImmutable<T>> {}
interface DeepImmutableMap<K, V> extends ReadonlyMap<DeepImmutable<K>, DeepImmutable<V>> {}
type DeepImmutableObject<T> = {
readonly [K in keyof T]: DeepImmutable<T[K]>
}
To verify that an object has no keys, use Record<string, never>
:
type EmptyObject = Record<string, never>;
const a: EmptyObject = {}; // ✅
const b: EmptyObject = { z : 'z' }; // ❌ Type 'string' is not assignable to type 'never'
Implementing feature flags via process.env.NODE_ENV
or process.env.APP_ENV
has downsides such as:
- Lack of granularity and control of specific singular features
- Some environments such as Next.js ignore the value for
process.env.NODE_ENV
Instead, a minimal feature flags implementation can be written in TypeScript:
- Generate a typed feature flags object, with constrained feature flag key names
- Feature flag default values are based on the environment (see comment in code below)
- Each of the feature defaults can be overridden by setting a related environment variable eg.
FEATURE_APP1_CLOUDINARY_DISABLED=true pnpm start
packages/common/util/featureFlags.ts
/**
* Get feature flags object with default values based on
* environment:
*
* - true in development
* - true in Playwright tests
* - true in Vitest tests
* - false in other environments, eg. production
*/
export function getFeatureFlags<
const FeatureFlags extends `FEATURE_${
| 'APP1'
| 'APP2'
| 'APP3'}_${string}_${
| 'ENABLED'
| 'DISABLED'
| 'ENABLED_INSECURE'
| 'DISABLED_INSECURE'}`[],
>(featureFlags: FeatureFlags) {
const isDevelopment = process.env.npm_lifecycle_event === 'dev';
const isPlaywright = !!process.env.PLAYWRIGHT;
const isVitest = !!process.env.VITEST;
return Object.fromEntries(
featureFlags.map((featureFlag) => [
featureFlag,
JSON.parse(
(typeof process !== 'undefined' &&
typeof process.env !== 'undefined' &&
typeof process.env[featureFlag] !== 'undefined'
? // Prefer process.env[featureFlag] if it is set
(process.env[featureFlag] as 'true' | 'false')
: // Otherwise, use the default value based on environment
isDevelopment || isPlaywright || isVitest
? 'true'
: 'false') satisfies 'true' | 'false',
),
]),
) as Record<FeatureFlags[number], boolean>;
}
Usage:
packages/app1/config.ts
import { getFeatureFlags } from '../common/util/featureFlags.js';
export const config = {
...getFeatureFlags([
'FEATURE_APP1_CLOUDINARY_DISABLED',
'FEATURE_APP1_RATE_LIMITING_DISABLED',
'FEATURE_APP1_TEST_RESPONSES_ENABLED_INSECURE',
]),
CLOUDINARY_API_KEY: process.env.CLOUDINARY_API_KEY as string,
PORT: process.env.PORT as string,
};
packages/app1/util/cloudinary.ts
import { config } from '../config.js';
cloudinary.v2.config({
api_key: config.CLOUDINARY_API_KEY,
// ...
});
// ...
export function deleteImageByPath(path: string) {
if (config.FEATURE_API_CLOUDINARY_DISABLED) return;
return cloudinary.v2.uploader.destroy(path);
}
JSON.stringify()
on an object with regular expressions as values will behave in an unual way:
JSON.stringify({
name: 'update',
urlRegex: /^\/cohorts\/[^/]+$/,
})
// '{"name":"update","urlRegex":{}}'
Use a custom replacer function to call .toString()
on the RegExp:
export function stringifyObjectWithRegexValues(obj: Record<string, unknown>) {
return JSON.stringify(obj, (key, value) => {
if (value instanceof RegExp) {
return value.toString();
}
return value;
});
}
This will return a visible representation of the regular expression:
stringifyObjectWithRegexValues({
name: 'update',
urlRegex: /^\/cohorts\/[^/]+$/,
})
// '{"name":"update","urlRegex":"/^\\\\/cohorts\\\\/[^/]+$/"}'
A generic type that allows for checking based on the name of the type ("opaque" type checking) as opposed to the data type ("transparent", the default in TypeScript).
Example:
type Username = Opaque<"Username", string>;
type Password = Opaque<"Password", string>;
function createUser(username: Username, password: Password) {}
const getUsername = () => getFormInput('username') as Username;
const getPassword = () => getFormInput('password') as Password;
createUser(
getUsername(),
getUsername(), // Error: Argument of type 'Opaque<"Username", string>' is not assignable to
// parameter of type 'Opaque<"Password", string>'.
);
Credit:
- @stereobooster in Pragmatic types: opaque types and how they could have saved Mars Climate Orbiter
- @phpnode in Stronger JavaScript with Opaque Types
type Opaque<K, T> = T & { __TYPE__: K };
A generic type that shows the final "resolved" type without indirection or abstraction.
const users = [
{ id: 1, name: "Jane" },
{ id: 2, name: "John" },
] as const;
type User = (typeof users)[number];
type LiteralToBase<T> = T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: T extends null
? null
: T extends undefined
? undefined
: T extends bigint
? bigint
: T extends symbol
? symbol
: T extends object
? object
: never;
type Widen<T> = {
[K in keyof T]: T[K] extends infer U ? LiteralToBase<U> : never;
};
export type Prettify<Type> = Type extends {}
? Type extends infer Obj
? Type extends Date
? Date
: { [Key in keyof Obj]: Prettify<Obj[Key]> } & {}
: never
: Type;
type WideUser = Widen<User>;
// ^? Widen<{ readonly id: 1; readonly name: "Jane"; }> | Widen<{ readonly id: 2; readonly name: "John"; }>
type PrettyWideUser = Prettify<Widen<User>>;
// ^? { readonly id: number; readonly name: string; } | { readonly id: number; readonly name: string; }
Credit:
export type Prettify<Type> = Type extends {}
? Type extends infer Obj
? Type extends Date
? Date
: { [Key in keyof Obj]: Prettify<Obj[Key]> } & {}
: never
: Type;
A generic type that allows for more soundness while using object spreads and Object.assign
.
type A = {
a: boolean;
b: number;
c: string;
};
type B = {
b: number[];
c: string[] | undefined;
d: string;
e: number | undefined;
};
type AB = Spread<A, B>;
// type AB = {
// a: boolean;
// b: number[];
// c: string | string[];
// d: string;
// e: number | undefined;
//};
Credit:
type Diff<T, U> = T extends U ? never : T; // Remove types from T that are assignable to U
// Names of properties in T with types that include undefined
type OptionalPropertyNames<T> =
{ [K in keyof T]: undefined extends T[K] ? K : never }[keyof T];
// Common properties from L and R with undefined in R[K] replaced by type in L[K]
type SpreadProperties<L, R, K extends keyof L & keyof R> =
{ [P in K]: L[P] | Diff<R[P], undefined> };
// Type of { ...L, ...R }
type Spread<L, R> =
// Properties in L that don't exist in R
& Pick<L, Diff<keyof L, keyof R>>
// Properties in R with types that exclude undefined
& Pick<R, Diff<keyof R, OptionalPropertyNames<R>>>
// Properties in R, with types that include undefined, that don't exist in L
& Pick<R, Diff<OptionalPropertyNames<R>, keyof L>>
// Properties in R, with types that include undefined, that exist in L
& SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>;
For higher quality utility types, you may have better luck with: