{"id":15648915,"url":"https://github.com/karlhorky/typescript-tricks","last_synced_at":"2025-06-22T04:02:50.341Z","repository":{"id":150431456,"uuid":"194509651","full_name":"karlhorky/typescript-tricks","owner":"karlhorky","description":"A collection of useful TypeScript tricks","archived":false,"fork":false,"pushed_at":"2024-09-01T15:45:04.000Z","size":47,"stargazers_count":42,"open_issues_count":3,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-13T21:33:52.300Z","etag":null,"topics":["typescript"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/karlhorky.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-06-30T11:45:32.000Z","updated_at":"2024-11-27T00:54:56.000Z","dependencies_parsed_at":"2024-10-20T00:24:02.607Z","dependency_job_id":null,"html_url":"https://github.com/karlhorky/typescript-tricks","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/karlhorky/typescript-tricks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karlhorky%2Ftypescript-tricks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karlhorky%2Ftypescript-tricks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karlhorky%2Ftypescript-tricks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karlhorky%2Ftypescript-tricks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/karlhorky","download_url":"https://codeload.github.com/karlhorky/typescript-tricks/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karlhorky%2Ftypescript-tricks/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261233098,"owners_count":23128194,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["typescript"],"created_at":"2024-10-03T12:27:05.134Z","updated_at":"2025-06-22T04:02:45.328Z","avatar_url":"https://github.com/karlhorky.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# TypeScript Tricks\n\nA collection of useful TypeScript tricks\n\n## `DeepImmutable` aka `DeepReadonly` Generic\n\nDeep immutable (readonly) generic type for specifying multi-level data structures that cannot be modified.\n\n**Example:**\n\n```ts\nlet deepX: DeepImmutable\u003c{y: {a: number}}\u003e = {y: {a: 1}};\ndeepX.y.a = 2; // Fails as expected!\n```\n\n**Credit:** [@nieltg](https://github.com/nieltg) in [Microsoft/TypeScript#13923 (comment)](https://github.com/Microsoft/TypeScript/issues/13923#issuecomment-402901005)\n\n```ts\ntype Primitive = undefined | null | boolean | string | number | Function\n\ntype Immutable\u003cT\u003e =\n  T extends Primitive ? T :\n    T extends Array\u003cinfer U\u003e ? ReadonlyArray\u003cU\u003e :\n      T extends Map\u003cinfer K, infer V\u003e ? ReadonlyMap\u003cK, V\u003e : Readonly\u003cT\u003e\n\ntype DeepImmutable\u003cT\u003e =\n  T extends Primitive ? T :\n    T extends Array\u003cinfer U\u003e ? DeepImmutableArray\u003cU\u003e :\n      T extends Map\u003cinfer K, infer V\u003e ? DeepImmutableMap\u003cK, V\u003e : DeepImmutableObject\u003cT\u003e\n\ninterface DeepImmutableArray\u003cT\u003e extends ReadonlyArray\u003cDeepImmutable\u003cT\u003e\u003e {}\ninterface DeepImmutableMap\u003cK, V\u003e extends ReadonlyMap\u003cDeepImmutable\u003cK\u003e, DeepImmutable\u003cV\u003e\u003e {}\ntype DeepImmutableObject\u003cT\u003e = {\n  readonly [K in keyof T]: DeepImmutable\u003cT[K]\u003e\n}\n```\n\n## Empty Object Type\n\nTo verify that an object has no keys, use `Record\u003cstring, never\u003e`:\n\n```ts\ntype EmptyObject = Record\u003cstring, never\u003e;\n\nconst a: EmptyObject = {}; // ✅\nconst b: EmptyObject = { z : 'z' }; // ❌ Type 'string' is not assignable to type 'never'\n```\n\n[Playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBAogtmUB5ARgKwgY2FAvFAJSwHsAnAEwB4BnYUgSwDsBzAGikYgDcJSA+ANwAoIZmKNaUAIYAuWAmTosOfAG8AvgKgB6bVECg5KPGSUc+IhCoM2PFFVQAXlDkByBy6iadewDLkQA)\n\n## Feature Flags\n\n[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:\n\n1. Lack of granularity and control of specific singular features\n2. [Some environments such as Next.js ignore the value for `process.env.NODE_ENV`](https://github.com/vercel/next.js/discussions/13410#discussioncomment-3663355)\n\nInstead, a minimal feature flags implementation can be written in TypeScript: \n\n- Generate a typed feature flags object, with constrained feature flag key names\n- Feature flag default values are based on the environment (see comment in code below)\n- Each of the feature defaults can be overridden by setting a related environment variable eg. `FEATURE_APP1_CLOUDINARY_DISABLED=true pnpm start`\n\n`packages/common/util/featureFlags.ts`\n\n```ts\n/**\n * Get feature flags object with default values based on\n * environment:\n *\n * - true in development\n * - true in Playwright tests\n * - true in Vitest tests\n * - false in other environments, eg. production\n */\nexport function getFeatureFlags\u003c\n  const FeatureFlags extends `FEATURE_${\n    | 'APP1'\n    | 'APP2'\n    | 'APP3'}_${string}_${\n    | 'ENABLED'\n    | 'DISABLED'\n    | 'ENABLED_INSECURE'\n    | 'DISABLED_INSECURE'}`[],\n\u003e(featureFlags: FeatureFlags) {\n  const isDevelopment = process.env.npm_lifecycle_event === 'dev';\n  const isPlaywright = !!process.env.PLAYWRIGHT;\n  const isVitest = !!process.env.VITEST;\n\n  return Object.fromEntries(\n    featureFlags.map((featureFlag) =\u003e [\n      featureFlag,\n      JSON.parse(\n        (typeof process !== 'undefined' \u0026\u0026\n        typeof process.env !== 'undefined' \u0026\u0026\n        typeof process.env[featureFlag] !== 'undefined'\n          ? // Prefer process.env[featureFlag] if it is set\n            (process.env[featureFlag] as 'true' | 'false')\n          : // Otherwise, use the default value based on environment\n            isDevelopment || isPlaywright || isVitest\n            ? 'true'\n            : 'false') satisfies 'true' | 'false',\n      ),\n    ]),\n  ) as Record\u003cFeatureFlags[number], boolean\u003e;\n}\n```\n\nUsage:\n\n`packages/app1/config.ts`\n\n```ts\nimport { getFeatureFlags } from '../common/util/featureFlags.js';\n\nexport const config = {\n  ...getFeatureFlags([\n    'FEATURE_APP1_CLOUDINARY_DISABLED',\n    'FEATURE_APP1_RATE_LIMITING_DISABLED',\n    'FEATURE_APP1_TEST_RESPONSES_ENABLED_INSECURE',\n  ]),\n  CLOUDINARY_API_KEY: process.env.CLOUDINARY_API_KEY as string,\n  PORT: process.env.PORT as string,\n};\n```\n\n`packages/app1/util/cloudinary.ts`\n\n```ts\nimport { config } from '../config.js';\n\ncloudinary.v2.config({\n  api_key: config.CLOUDINARY_API_KEY,\n  // ...\n});\n\n// ...\n\nexport function deleteImageByPath(path: string) {\n  if (config.FEATURE_API_CLOUDINARY_DISABLED) return;\n  return cloudinary.v2.uploader.destroy(path);\n}\n```\n\n## `JSON.stringify()` an Object with Regular Expression Values\n\n`JSON.stringify()` on an object with regular expressions as values will behave in an unual way:\n\n```ts\nJSON.stringify({\n  name: 'update',\n  urlRegex: /^\\/cohorts\\/[^/]+$/,\n})\n// '{\"name\":\"update\",\"urlRegex\":{}}'\n```\n\nUse a custom replacer function to call `.toString()` on the RegExp:\n\n```ts\nexport function stringifyObjectWithRegexValues(obj: Record\u003cstring, unknown\u003e) {\n  return JSON.stringify(obj, (key, value) =\u003e {\n    if (value instanceof RegExp) {\n      return value.toString();\n    }\n    return value;\n  });\n}\n```\n\nThis will return a visible representation of the regular expression:\n\n```ts\nstringifyObjectWithRegexValues({\n  name: 'update',\n  urlRegex: /^\\/cohorts\\/[^/]+$/,\n})\n// '{\"name\":\"update\",\"urlRegex\":\"/^\\\\\\\\/cohorts\\\\\\\\/[^/]+$/\"}'\n```\n\n## `Opaque` Generic\n\nA 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).\n\n**Example:**\n\n```ts\ntype Username = Opaque\u003c\"Username\", string\u003e;\ntype Password = Opaque\u003c\"Password\", string\u003e;\n\nfunction createUser(username: Username, password: Password) {}\nconst getUsername = () =\u003e getFormInput('username') as Username;\nconst getPassword = () =\u003e getFormInput('password') as Password;\n\ncreateUser(\n  getUsername(),\n  getUsername(),  // Error: Argument of type 'Opaque\u003c\"Username\", string\u003e' is not assignable to\n                  // parameter of type 'Opaque\u003c\"Password\", string\u003e'.\n);\n```\n\n**Credit:**\n\n- [@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)\n- [@phpnode](https://twitter.com/phpnode) in [Stronger JavaScript with Opaque Types](https://codemix.com/opaque-types-in-javascript/)\n\n```ts\ntype Opaque\u003cK, T\u003e = T \u0026 { __TYPE__: K };\n```\n\n## `Prettify` Generic\n\nA generic type that shows the final \"resolved\" type without indirection or abstraction.\n\n```ts\nconst users = [\n  { id: 1, name: \"Jane\" },\n  { id: 2, name: \"John\" },\n] as const;\n\ntype User = (typeof users)[number];\n\ntype LiteralToBase\u003cT\u003e = T extends string\n  ? string\n  : T extends number\n  ? number\n  : T extends boolean\n  ? boolean\n  : T extends null\n  ? null\n  : T extends undefined\n  ? undefined\n  : T extends bigint\n  ? bigint\n  : T extends symbol\n  ? symbol\n  : T extends object\n  ? object\n  : never;\n\ntype Widen\u003cT\u003e = {\n  [K in keyof T]: T[K] extends infer U ? LiteralToBase\u003cU\u003e : never;\n};\n\nexport type Prettify\u003cType\u003e = Type extends {}\n  ? Type extends infer Obj\n    ? Type extends Date\n      ? Date\n      : { [Key in keyof Obj]: Prettify\u003cObj[Key]\u003e } \u0026 {}\n    : never\n  : Type;\n\ntype WideUser = Widen\u003cUser\u003e;\n//   ^? Widen\u003c{ readonly id: 1; readonly name: \"Jane\"; }\u003e | Widen\u003c{ readonly id: 2; readonly name: \"John\"; }\u003e\n\ntype PrettyWideUser = Prettify\u003cWiden\u003cUser\u003e\u003e;\n//   ^? { readonly id: number; readonly name: string; } | { readonly id: number; readonly name: string; }\n```\n\n**Credit:**\n\n- [Matt Pocock](https://twitter.com/mattpocockuk) in [`Prettify` type helper tweet](https://twitter.com/mattpocockuk/status/1622730173446557697)\n- [Aaron](https://twitter.com/nowlena) in [`Prettify` type helper tweet reply](https://twitter.com/nowlena/status/1622967286188630020)\n\n```ts\nexport type Prettify\u003cType\u003e = Type extends {}\n  ? Type extends infer Obj\n    ? Type extends Date\n      ? Date\n      : { [Key in keyof Obj]: Prettify\u003cObj[Key]\u003e } \u0026 {}\n    : never\n  : Type;\n```\n\n## `Spread` Generic\n\nA generic type that allows for [more soundness](https://github.com/microsoft/TypeScript/pull/28553#issuecomment-440004598) while using object spreads and `Object.assign`.\n\n```ts\ntype A = {\n    a: boolean;\n    b: number;\n    c: string;\n};\n\ntype B = {\n    b: number[];\n    c: string[] | undefined;\n    d: string;\n    e: number | undefined;\n};\n\ntype AB = Spread\u003cA, B\u003e;\n\n// type AB = {\n//    a: boolean;\n//    b: number[];\n//    c: string | string[];\n//    d: string;\n//    e: number | undefined;\n//};\n```\n\n**Credit:**\n\n- [@ahejlsberg](https://github.com/ahejlsberg) in [Microsoft/TypeScript#21316 (comment)](https://github.com/Microsoft/TypeScript/pull/21316#issuecomment-359574388)\n\n```ts\ntype Diff\u003cT, U\u003e = T extends U ? never : T;  // Remove types from T that are assignable to U\n\n// Names of properties in T with types that include undefined\ntype OptionalPropertyNames\u003cT\u003e =\n    { [K in keyof T]: undefined extends T[K] ? K : never }[keyof T];\n\n// Common properties from L and R with undefined in R[K] replaced by type in L[K]\ntype SpreadProperties\u003cL, R, K extends keyof L \u0026 keyof R\u003e =\n    { [P in K]: L[P] | Diff\u003cR[P], undefined\u003e };\n\n// Type of { ...L, ...R }\ntype Spread\u003cL, R\u003e =\n    // Properties in L that don't exist in R\n    \u0026 Pick\u003cL, Diff\u003ckeyof L, keyof R\u003e\u003e\n    // Properties in R with types that exclude undefined\n    \u0026 Pick\u003cR, Diff\u003ckeyof R, OptionalPropertyNames\u003cR\u003e\u003e\u003e\n    // Properties in R, with types that include undefined, that don't exist in L\n    \u0026 Pick\u003cR, Diff\u003cOptionalPropertyNames\u003cR\u003e, keyof L\u003e\u003e\n    // Properties in R, with types that include undefined, that exist in L\n    \u0026 SpreadProperties\u003cL, R, OptionalPropertyNames\u003cR\u003e \u0026 keyof L\u003e;\n```\n\n## Related\n\nFor higher quality utility types, you may have better luck with:\n\n- https://github.com/millsp/ts-toolbelt\n- https://github.com/gcanti/typelevel-ts\n- https://github.com/pelotom/type-zoo\n- https://github.com/kgtkr/typepark\n- https://github.com/tycho01/typical\n- https://github.com/piotrwitek/utility-types\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkarlhorky%2Ftypescript-tricks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkarlhorky%2Ftypescript-tricks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkarlhorky%2Ftypescript-tricks/lists"}