Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/karlhorky/typescript-tricks

A collection of useful TypeScript tricks
https://github.com/karlhorky/typescript-tricks

typescript

Last synced: 3 months ago
JSON representation

A collection of useful TypeScript tricks

Awesome Lists containing this project

README

        

# TypeScript Tricks

A collection of useful TypeScript tricks

## `DeepImmutable` aka `DeepReadonly` Generic

Deep immutable (readonly) generic type for specifying multi-level data structures that cannot be modified.

**Example:**

```ts
let deepX: DeepImmutable<{y: {a: number}}> = {y: {a: 1}};
deepX.y.a = 2; // Fails as expected!
```

**Credit:** [@nieltg](https://github.com/nieltg) in [Microsoft/TypeScript#13923 (comment)](https://github.com/Microsoft/TypeScript/issues/13923#issuecomment-402901005)

```ts
type Primitive = undefined | null | boolean | string | number | Function

type Immutable =
T extends Primitive ? T :
T extends Array ? ReadonlyArray :
T extends Map ? ReadonlyMap : Readonly

type DeepImmutable =
T extends Primitive ? T :
T extends Array ? DeepImmutableArray :
T extends Map ? DeepImmutableMap : DeepImmutableObject

interface DeepImmutableArray extends ReadonlyArray> {}
interface DeepImmutableMap extends ReadonlyMap, DeepImmutable> {}
type DeepImmutableObject = {
readonly [K in keyof T]: DeepImmutable
}
```

## Empty Object Type

To verify that an object has no keys, use `Record`:

```ts
type EmptyObject = Record;

const a: EmptyObject = {}; // ✅
const b: EmptyObject = { z : 'z' }; // ❌ Type 'string' is not assignable to type 'never'
```

[Playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAogtmUB5ARgKwgY2FAvFAJSwHsAnAEwB4BnYUgSwDsBzAGikYgDcJSA+ANwAoIZmKNaUAIYAuWAmTosOfAG8AvgKgB6bVECg5KPGSUc+IhCoM2PFFVQAXlDkByBy6iadewDLkQA)

## Feature Flags

[Implementing feature flags via `process.env.NODE_ENV` or `process.env.APP_ENV` has downsides](https://ricostacruz.com/posts/feature-flags#alternative-feature-flags) such as:

1. Lack of granularity and control of specific singular features
2. [Some environments such as Next.js ignore the value for `process.env.NODE_ENV`](https://github.com/vercel/next.js/discussions/13410#discussioncomment-3663355)

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`

```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;
}
```

Usage:

`packages/app1/config.ts`

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

```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()` an Object with Regular Expression Values

`JSON.stringify()` on an object with regular expressions as values will behave in an unual way:

```ts
JSON.stringify({
name: 'update',
urlRegex: /^\/cohorts\/[^/]+$/,
})
// '{"name":"update","urlRegex":{}}'
```

Use a custom replacer function to call `.toString()` on the RegExp:

```ts
export function stringifyObjectWithRegexValues(obj: Record) {
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:

```ts
stringifyObjectWithRegexValues({
name: 'update',
urlRegex: /^\/cohorts\/[^/]+$/,
})
// '{"name":"update","urlRegex":"/^\\\\/cohorts\\\\/[^/]+$/"}'
```

## `Opaque` Generic

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

```ts
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](https://twitter.com/stereobooster) in [Pragmatic types: opaque types and how they could have saved Mars Climate Orbiter](https://dev.to/stereobooster/pragmatic-types-opaque-types-and-how-they-could-have-saved-mars-climate-orbiter-1551)
- [@phpnode](https://twitter.com/phpnode) in [Stronger JavaScript with Opaque Types](https://codemix.com/opaque-types-in-javascript/)

```ts
type Opaque = T & { __TYPE__: K };
```

## `Prettify` Generic

A generic type that shows the final "resolved" type without indirection or abstraction.

```ts
const users = [
{ id: 1, name: "Jane" },
{ id: 2, name: "John" },
] as const;

type User = (typeof users)[number];

type LiteralToBase = 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 = {
[K in keyof T]: T[K] extends infer U ? LiteralToBase : never;
};

export type Prettify = Type extends {}
? Type extends infer Obj
? Type extends Date
? Date
: { [Key in keyof Obj]: Prettify } & {}
: never
: Type;

type WideUser = Widen;
// ^? Widen<{ readonly id: 1; readonly name: "Jane"; }> | Widen<{ readonly id: 2; readonly name: "John"; }>

type PrettyWideUser = Prettify>;
// ^? { readonly id: number; readonly name: string; } | { readonly id: number; readonly name: string; }
```

**Credit:**

- [Matt Pocock](https://twitter.com/mattpocockuk) in [`Prettify` type helper tweet](https://twitter.com/mattpocockuk/status/1622730173446557697)
- [Aaron](https://twitter.com/nowlena) in [`Prettify` type helper tweet reply](https://twitter.com/nowlena/status/1622967286188630020)

```ts
export type Prettify = Type extends {}
? Type extends infer Obj
? Type extends Date
? Date
: { [Key in keyof Obj]: Prettify } & {}
: never
: Type;
```

## `Spread` Generic

A generic type that allows for [more soundness](https://github.com/microsoft/TypeScript/pull/28553#issuecomment-440004598) while using object spreads and `Object.assign`.

```ts
type A = {
a: boolean;
b: number;
c: string;
};

type B = {
b: number[];
c: string[] | undefined;
d: string;
e: number | undefined;
};

type AB = Spread;

// type AB = {
// a: boolean;
// b: number[];
// c: string | string[];
// d: string;
// e: number | undefined;
//};
```

**Credit:**

- [@ahejlsberg](https://github.com/ahejlsberg) in [Microsoft/TypeScript#21316 (comment)](https://github.com/Microsoft/TypeScript/pull/21316#issuecomment-359574388)

```ts
type Diff = 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 =
{ [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 =
{ [P in K]: L[P] | Diff };

// Type of { ...L, ...R }
type Spread =
// Properties in L that don't exist in R
& Pick>
// Properties in R with types that exclude undefined
& Pick>>
// Properties in R, with types that include undefined, that don't exist in L
& Pick, keyof L>>
// Properties in R, with types that include undefined, that exist in L
& SpreadProperties & keyof L>;
```

## Related

For higher quality utility types, you may have better luck with:

- https://github.com/millsp/ts-toolbelt
- https://github.com/gcanti/typelevel-ts
- https://github.com/pelotom/type-zoo
- https://github.com/kgtkr/typepark
- https://github.com/tycho01/typical
- https://github.com/piotrwitek/utility-types