@prairielearn/utils 2.0.5 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @prairielearn/utils
2
2
 
3
+ ## 3.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5b466e0: Add type utilities: `WithRequiredKeys`, `ExpandRecursively`, `assertNever`, `Result`, `Brand`, `withBrand`, `IsUnion`, `Prettify`, and `MergeUnion`
8
+
9
+ ## 3.0.0
10
+
11
+ ### Major Changes
12
+
13
+ - 3914bb4: Upgrade to Node 24
14
+
3
15
  ## 2.0.5
4
16
 
5
17
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1,3 +1,99 @@
1
+ /**
2
+ * Produces a type that only includes the nullable properties of T.
3
+ */
4
+ type NullableProperties<T> = {
5
+ [K in keyof T as null extends T[K] ? K : never]: T[K];
6
+ };
7
+ /**
8
+ * Produces a type containing the keys of T that are nullable.
9
+ */
10
+ type NullableKeys<T> = keyof NullableProperties<T>;
11
+ /**
12
+ * Produces a type with the same keys as T. All properties are marked as required,
13
+ * non-nullable, and not undefined.
14
+ */
15
+ type RequiredProperty<T> = {
16
+ [P in keyof T]-?: NonNullable<T[P]>;
17
+ };
18
+ /**
19
+ * Produces a type with the same keys as T. If a key is in `RequiredKeys`, it will
20
+ * be marked as non-optional and non-nullable. Otherwise, it will be marked as
21
+ * optional with a type of `undefined`.
22
+ *
23
+ * ```ts
24
+ * type Foo = { a: string; b?: number; c: null };
25
+ * type Bar = WithRequiredKeys<Foo, 'a' | 'b'>;
26
+ * // Bar is equivalent to { a: string; b: number; c?: undefined; }
27
+ * ```
28
+ */
29
+ export type WithRequiredKeys<T, RequiredKeys extends keyof T> = Omit<T, RequiredKeys | NullableKeys<T>> & RequiredProperty<Pick<T, RequiredKeys>> & Partial<Record<NullableKeys<Omit<T, RequiredKeys>>, undefined>>;
30
+ /**
31
+ * Useful for convincing an IDE to show the expansion of a type.
32
+ *
33
+ * ```ts
34
+ * type Foo = { a: string; b?: number; c: null };
35
+ * type Bar = ExpandRecursively<Foo>;
36
+ * ```
37
+ */
38
+ export type ExpandRecursively<T> = T extends object ? T extends infer O ? {
39
+ [K in keyof O]: ExpandRecursively<O[K]>;
40
+ } : never : T;
41
+ export declare function assertNever(value: never): never;
42
+ export type Result<T, E = Error> = {
43
+ success: true;
44
+ value: T;
45
+ } | {
46
+ success: false;
47
+ error: E;
48
+ };
49
+ declare const __brand: unique symbol;
50
+ export type Brand<K, T> = K & {
51
+ [__brand]: T;
52
+ };
53
+ /**
54
+ * Applies a brand to a value. This is a type-safe identity function that
55
+ * allows you to create branded types from their underlying values.
56
+ *
57
+ * ```ts
58
+ * type UserId = Brand<string, 'UserId'>;
59
+ * const userId = withBrand<UserId>('123'); // userId has type UserId
60
+ * ```
61
+ */
62
+ export declare function withBrand<B extends Brand<any, any>>(value: B extends Brand<infer K, any> ? Omit<K, typeof __brand> : never): B;
63
+ /**
64
+ * Detects if T is a union type (e.g., 'a' | 'b') vs a single literal (e.g., 'a')
65
+ */
66
+ export type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;
67
+ /**
68
+ * The Prettify helper is a utility type that takes an object type and makes the hover overlay more readable.
69
+ *
70
+ * https://www.totaltypescript.com/concepts/the-prettify-helper
71
+ */
72
+ export type Prettify<T> = {
73
+ [K in keyof T]: T[K];
74
+ } & {};
75
+ type UnionKeys<T> = T extends any ? keyof T : never;
76
+ type KeysInAllMembers<T> = {
77
+ [K in UnionKeys<T>]: [T] extends [Record<K, any>] ? K : never;
78
+ }[UnionKeys<T>];
79
+ type KeysInSomeMembers<T> = Exclude<UnionKeys<T>, KeysInAllMembers<T>>;
80
+ type UnionPropValue<T, K extends PropertyKey> = T extends any ? K extends keyof T ? T[K] : never : never;
81
+ /**
82
+ * Merges a union of object types into a single object type.
83
+ * Keys present in all members become required, keys present in some become optional.
84
+ *
85
+ * ```ts
86
+ * type A = { id: string; name: string };
87
+ * type B = { id: string; age: number };
88
+ * type Merged = MergeUnion<A | B>;
89
+ * // Merged is { id: string } & { name?: string; age?: number }
90
+ * ```
91
+ */
92
+ export type MergeUnion<T> = {
93
+ [K in KeysInAllMembers<T>]: UnionPropValue<T, K>;
94
+ } & {
95
+ [K in KeysInSomeMembers<T>]?: UnionPropValue<T, K>;
96
+ };
1
97
  /**
2
98
  * An object containing a promise and its resolve/reject methods.
3
99
  */
@@ -16,4 +112,5 @@ export interface PromiseWithResolvers<T> {
16
112
  * @returns An object containing the promise, resolve, and reject.
17
113
  */
18
114
  export declare function withResolvers<T>(): PromiseWithResolvers<T>;
115
+ export {};
19
116
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,4BAA4B;IAC5B,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAC7C,2BAA2B;IAC3B,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,KAAK,oBAAoB,CAAC,CAAC,CAAC,CAQ1D","sourcesContent":["/**\n * An object containing a promise and its resolve/reject methods.\n */\nexport interface PromiseWithResolvers<T> {\n /** The promise instance. */\n promise: Promise<T>;\n /** Resolves the promise. */\n resolve: (value: T | PromiseLike<T>) => void;\n /** Rejects the promise. */\n reject: (reason?: any) => void;\n}\n\n/**\n * Returns an object with a promise and its resolve/reject methods exposed.\n * This is similar to Node.js's util.withResolvers (Node 21+).\n *\n * @returns An object containing the promise, resolve, and reject.\n */\nexport function withResolvers<T>(): PromiseWithResolvers<T> {\n let resolve: (value: T | PromiseLike<T>) => void;\n let reject: (reason?: any) => void;\n const promise = new Promise<T>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n return { promise, resolve: resolve!, reject: reject! };\n}\n"]}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Produces a type that only includes the nullable properties of T.\n */\ntype NullableProperties<T> = {\n [K in keyof T as null extends T[K] ? K : never]: T[K];\n};\n\n/**\n * Produces a type containing the keys of T that are nullable.\n */\ntype NullableKeys<T> = keyof NullableProperties<T>;\n\n/**\n * Produces a type with the same keys as T. All properties are marked as required,\n * non-nullable, and not undefined.\n */\ntype RequiredProperty<T> = { [P in keyof T]-?: NonNullable<T[P]> };\n\n/**\n * Produces a type with the same keys as T. If a key is in `RequiredKeys`, it will\n * be marked as non-optional and non-nullable. Otherwise, it will be marked as\n * optional with a type of `undefined`.\n *\n * ```ts\n * type Foo = { a: string; b?: number; c: null };\n * type Bar = WithRequiredKeys<Foo, 'a' | 'b'>;\n * // Bar is equivalent to { a: string; b: number; c?: undefined; }\n * ```\n */\nexport type WithRequiredKeys<T, RequiredKeys extends keyof T> = Omit<\n T,\n RequiredKeys | NullableKeys<T>\n> &\n RequiredProperty<Pick<T, RequiredKeys>> &\n Partial<Record<NullableKeys<Omit<T, RequiredKeys>>, undefined>>;\n\n/**\n * Useful for convincing an IDE to show the expansion of a type.\n *\n * ```ts\n * type Foo = { a: string; b?: number; c: null };\n * type Bar = ExpandRecursively<Foo>;\n * ```\n */\nexport type ExpandRecursively<T> = T extends object\n ? T extends infer O\n ? { [K in keyof O]: ExpandRecursively<O[K]> }\n : never\n : T;\n\nexport function assertNever(value: never): never {\n throw new Error(`Unexpected value: ${value}`);\n}\n\nexport type Result<T, E = Error> = { success: true; value: T } | { success: false; error: E };\n\ndeclare const __brand: unique symbol;\nexport type Brand<K, T> = K & { [__brand]: T };\n\n/**\n * Applies a brand to a value. This is a type-safe identity function that\n * allows you to create branded types from their underlying values.\n *\n * ```ts\n * type UserId = Brand<string, 'UserId'>;\n * const userId = withBrand<UserId>('123'); // userId has type UserId\n * ```\n */\nexport function withBrand<B extends Brand<any, any>>(\n value: B extends Brand<infer K, any> ? Omit<K, typeof __brand> : never,\n): B {\n return value as B;\n}\n\n/**\n * Detects if T is a union type (e.g., 'a' | 'b') vs a single literal (e.g., 'a')\n */\nexport type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\n\n/**\n * The Prettify helper is a utility type that takes an object type and makes the hover overlay more readable.\n *\n * https://www.totaltypescript.com/concepts/the-prettify-helper\n */\nexport type Prettify<T> = {\n [K in keyof T]: T[K];\n} & {};\n\n// All keys from any member of union T\ntype UnionKeys<T> = T extends any ? keyof T : never;\n\n// Keys that exist in ALL members of union T\ntype KeysInAllMembers<T> = {\n [K in UnionKeys<T>]: [T] extends [Record<K, any>] ? K : never;\n}[UnionKeys<T>];\n\n// Keys that exist in SOME but not ALL members\ntype KeysInSomeMembers<T> = Exclude<UnionKeys<T>, KeysInAllMembers<T>>;\n\n// Value type for key K across all members that have it\ntype UnionPropValue<T, K extends PropertyKey> = T extends any\n ? K extends keyof T\n ? T[K]\n : never\n : never;\n\n/**\n * Merges a union of object types into a single object type.\n * Keys present in all members become required, keys present in some become optional.\n *\n * ```ts\n * type A = { id: string; name: string };\n * type B = { id: string; age: number };\n * type Merged = MergeUnion<A | B>;\n * // Merged is { id: string } & { name?: string; age?: number }\n * ```\n */\nexport type MergeUnion<T> = {\n [K in KeysInAllMembers<T>]: UnionPropValue<T, K>;\n} & {\n [K in KeysInSomeMembers<T>]?: UnionPropValue<T, K>;\n};\n\n/**\n * An object containing a promise and its resolve/reject methods.\n */\nexport interface PromiseWithResolvers<T> {\n /** The promise instance. */\n promise: Promise<T>;\n /** Resolves the promise. */\n resolve: (value: T | PromiseLike<T>) => void;\n /** Rejects the promise. */\n reject: (reason?: any) => void;\n}\n\n/**\n * Returns an object with a promise and its resolve/reject methods exposed.\n * This is similar to Node.js's util.withResolvers (Node 21+).\n *\n * @returns An object containing the promise, resolve, and reject.\n */\nexport function withResolvers<T>(): PromiseWithResolvers<T> {\n let resolve: (value: T | PromiseLike<T>) => void;\n let reject: (reason?: any) => void;\n const promise = new Promise<T>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n return { promise, resolve: resolve!, reject: reject! };\n}\n"]}
package/dist/index.js CHANGED
@@ -1,3 +1,18 @@
1
+ export function assertNever(value) {
2
+ throw new Error(`Unexpected value: ${value}`);
3
+ }
4
+ /**
5
+ * Applies a brand to a value. This is a type-safe identity function that
6
+ * allows you to create branded types from their underlying values.
7
+ *
8
+ * ```ts
9
+ * type UserId = Brand<string, 'UserId'>;
10
+ * const userId = withBrand<UserId>('123'); // userId has type UserId
11
+ * ```
12
+ */
13
+ export function withBrand(value) {
14
+ return value;
15
+ }
1
16
  /**
2
17
  * Returns an object with a promise and its resolve/reject methods exposed.
3
18
  * This is similar to Node.js's util.withResolvers (Node 21+).
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAYA;;;;;GAKG;AACH,MAAM,UAAU,aAAa,GAA+B;IAC1D,IAAI,OAA4C,CAAC;IACjD,IAAI,MAA8B,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;QAC3C,OAAO,GAAG,GAAG,CAAC;QACd,MAAM,GAAG,GAAG,CAAC;IAAA,CACd,CAAC,CAAC;IACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAQ,EAAE,MAAM,EAAE,MAAO,EAAE,CAAC;AAAA,CACxD","sourcesContent":["/**\n * An object containing a promise and its resolve/reject methods.\n */\nexport interface PromiseWithResolvers<T> {\n /** The promise instance. */\n promise: Promise<T>;\n /** Resolves the promise. */\n resolve: (value: T | PromiseLike<T>) => void;\n /** Rejects the promise. */\n reject: (reason?: any) => void;\n}\n\n/**\n * Returns an object with a promise and its resolve/reject methods exposed.\n * This is similar to Node.js's util.withResolvers (Node 21+).\n *\n * @returns An object containing the promise, resolve, and reject.\n */\nexport function withResolvers<T>(): PromiseWithResolvers<T> {\n let resolve: (value: T | PromiseLike<T>) => void;\n let reject: (reason?: any) => void;\n const promise = new Promise<T>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n return { promise, resolve: resolve!, reject: reject! };\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkDA,MAAM,UAAU,WAAW,CAAC,KAAY,EAAS;IAC/C,MAAM,IAAI,KAAK,CAAC,qBAAqB,KAAK,EAAE,CAAC,CAAC;AAAA,CAC/C;AAOD;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CACvB,KAAsE,EACnE;IACH,OAAO,KAAU,CAAC;AAAA,CACnB;AA+DD;;;;;GAKG;AACH,MAAM,UAAU,aAAa,GAA+B;IAC1D,IAAI,OAA4C,CAAC;IACjD,IAAI,MAA8B,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;QAC3C,OAAO,GAAG,GAAG,CAAC;QACd,MAAM,GAAG,GAAG,CAAC;IAAA,CACd,CAAC,CAAC;IACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAQ,EAAE,MAAM,EAAE,MAAO,EAAE,CAAC;AAAA,CACxD","sourcesContent":["/**\n * Produces a type that only includes the nullable properties of T.\n */\ntype NullableProperties<T> = {\n [K in keyof T as null extends T[K] ? K : never]: T[K];\n};\n\n/**\n * Produces a type containing the keys of T that are nullable.\n */\ntype NullableKeys<T> = keyof NullableProperties<T>;\n\n/**\n * Produces a type with the same keys as T. All properties are marked as required,\n * non-nullable, and not undefined.\n */\ntype RequiredProperty<T> = { [P in keyof T]-?: NonNullable<T[P]> };\n\n/**\n * Produces a type with the same keys as T. If a key is in `RequiredKeys`, it will\n * be marked as non-optional and non-nullable. Otherwise, it will be marked as\n * optional with a type of `undefined`.\n *\n * ```ts\n * type Foo = { a: string; b?: number; c: null };\n * type Bar = WithRequiredKeys<Foo, 'a' | 'b'>;\n * // Bar is equivalent to { a: string; b: number; c?: undefined; }\n * ```\n */\nexport type WithRequiredKeys<T, RequiredKeys extends keyof T> = Omit<\n T,\n RequiredKeys | NullableKeys<T>\n> &\n RequiredProperty<Pick<T, RequiredKeys>> &\n Partial<Record<NullableKeys<Omit<T, RequiredKeys>>, undefined>>;\n\n/**\n * Useful for convincing an IDE to show the expansion of a type.\n *\n * ```ts\n * type Foo = { a: string; b?: number; c: null };\n * type Bar = ExpandRecursively<Foo>;\n * ```\n */\nexport type ExpandRecursively<T> = T extends object\n ? T extends infer O\n ? { [K in keyof O]: ExpandRecursively<O[K]> }\n : never\n : T;\n\nexport function assertNever(value: never): never {\n throw new Error(`Unexpected value: ${value}`);\n}\n\nexport type Result<T, E = Error> = { success: true; value: T } | { success: false; error: E };\n\ndeclare const __brand: unique symbol;\nexport type Brand<K, T> = K & { [__brand]: T };\n\n/**\n * Applies a brand to a value. This is a type-safe identity function that\n * allows you to create branded types from their underlying values.\n *\n * ```ts\n * type UserId = Brand<string, 'UserId'>;\n * const userId = withBrand<UserId>('123'); // userId has type UserId\n * ```\n */\nexport function withBrand<B extends Brand<any, any>>(\n value: B extends Brand<infer K, any> ? Omit<K, typeof __brand> : never,\n): B {\n return value as B;\n}\n\n/**\n * Detects if T is a union type (e.g., 'a' | 'b') vs a single literal (e.g., 'a')\n */\nexport type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;\n\n/**\n * The Prettify helper is a utility type that takes an object type and makes the hover overlay more readable.\n *\n * https://www.totaltypescript.com/concepts/the-prettify-helper\n */\nexport type Prettify<T> = {\n [K in keyof T]: T[K];\n} & {};\n\n// All keys from any member of union T\ntype UnionKeys<T> = T extends any ? keyof T : never;\n\n// Keys that exist in ALL members of union T\ntype KeysInAllMembers<T> = {\n [K in UnionKeys<T>]: [T] extends [Record<K, any>] ? K : never;\n}[UnionKeys<T>];\n\n// Keys that exist in SOME but not ALL members\ntype KeysInSomeMembers<T> = Exclude<UnionKeys<T>, KeysInAllMembers<T>>;\n\n// Value type for key K across all members that have it\ntype UnionPropValue<T, K extends PropertyKey> = T extends any\n ? K extends keyof T\n ? T[K]\n : never\n : never;\n\n/**\n * Merges a union of object types into a single object type.\n * Keys present in all members become required, keys present in some become optional.\n *\n * ```ts\n * type A = { id: string; name: string };\n * type B = { id: string; age: number };\n * type Merged = MergeUnion<A | B>;\n * // Merged is { id: string } & { name?: string; age?: number }\n * ```\n */\nexport type MergeUnion<T> = {\n [K in KeysInAllMembers<T>]: UnionPropValue<T, K>;\n} & {\n [K in KeysInSomeMembers<T>]?: UnionPropValue<T, K>;\n};\n\n/**\n * An object containing a promise and its resolve/reject methods.\n */\nexport interface PromiseWithResolvers<T> {\n /** The promise instance. */\n promise: Promise<T>;\n /** Resolves the promise. */\n resolve: (value: T | PromiseLike<T>) => void;\n /** Rejects the promise. */\n reject: (reason?: any) => void;\n}\n\n/**\n * Returns an object with a promise and its resolve/reject methods exposed.\n * This is similar to Node.js's util.withResolvers (Node 21+).\n *\n * @returns An object containing the promise, resolve, and reject.\n */\nexport function withResolvers<T>(): PromiseWithResolvers<T> {\n let resolve: (value: T | PromiseLike<T>) => void;\n let reject: (reason?: any) => void;\n const promise = new Promise<T>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n return { promise, resolve: resolve!, reject: reject! };\n}\n"]}
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@prairielearn/utils",
3
- "version": "2.0.5",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/PrairieLearn/PrairieLearn.git",
8
8
  "directory": "packages/utils"
9
9
  },
10
+ "engines": {
11
+ "node": ">=24.0.0"
12
+ },
10
13
  "main": "./dist/index.js",
11
14
  "scripts": {
12
15
  "build": "tsgo",
@@ -15,7 +18,7 @@
15
18
  },
16
19
  "devDependencies": {
17
20
  "@prairielearn/tsconfig": "^0.0.0",
18
- "@types/node": "^22.19.5",
21
+ "@types/node": "^24.10.9",
19
22
  "@typescript/native-preview": "^7.0.0-dev.20260106.1",
20
23
  "@vitest/coverage-v8": "^4.0.17",
21
24
  "tsx": "^4.21.0",
package/src/index.ts CHANGED
@@ -1,3 +1,126 @@
1
+ /**
2
+ * Produces a type that only includes the nullable properties of T.
3
+ */
4
+ type NullableProperties<T> = {
5
+ [K in keyof T as null extends T[K] ? K : never]: T[K];
6
+ };
7
+
8
+ /**
9
+ * Produces a type containing the keys of T that are nullable.
10
+ */
11
+ type NullableKeys<T> = keyof NullableProperties<T>;
12
+
13
+ /**
14
+ * Produces a type with the same keys as T. All properties are marked as required,
15
+ * non-nullable, and not undefined.
16
+ */
17
+ type RequiredProperty<T> = { [P in keyof T]-?: NonNullable<T[P]> };
18
+
19
+ /**
20
+ * Produces a type with the same keys as T. If a key is in `RequiredKeys`, it will
21
+ * be marked as non-optional and non-nullable. Otherwise, it will be marked as
22
+ * optional with a type of `undefined`.
23
+ *
24
+ * ```ts
25
+ * type Foo = { a: string; b?: number; c: null };
26
+ * type Bar = WithRequiredKeys<Foo, 'a' | 'b'>;
27
+ * // Bar is equivalent to { a: string; b: number; c?: undefined; }
28
+ * ```
29
+ */
30
+ export type WithRequiredKeys<T, RequiredKeys extends keyof T> = Omit<
31
+ T,
32
+ RequiredKeys | NullableKeys<T>
33
+ > &
34
+ RequiredProperty<Pick<T, RequiredKeys>> &
35
+ Partial<Record<NullableKeys<Omit<T, RequiredKeys>>, undefined>>;
36
+
37
+ /**
38
+ * Useful for convincing an IDE to show the expansion of a type.
39
+ *
40
+ * ```ts
41
+ * type Foo = { a: string; b?: number; c: null };
42
+ * type Bar = ExpandRecursively<Foo>;
43
+ * ```
44
+ */
45
+ export type ExpandRecursively<T> = T extends object
46
+ ? T extends infer O
47
+ ? { [K in keyof O]: ExpandRecursively<O[K]> }
48
+ : never
49
+ : T;
50
+
51
+ export function assertNever(value: never): never {
52
+ throw new Error(`Unexpected value: ${value}`);
53
+ }
54
+
55
+ export type Result<T, E = Error> = { success: true; value: T } | { success: false; error: E };
56
+
57
+ declare const __brand: unique symbol;
58
+ export type Brand<K, T> = K & { [__brand]: T };
59
+
60
+ /**
61
+ * Applies a brand to a value. This is a type-safe identity function that
62
+ * allows you to create branded types from their underlying values.
63
+ *
64
+ * ```ts
65
+ * type UserId = Brand<string, 'UserId'>;
66
+ * const userId = withBrand<UserId>('123'); // userId has type UserId
67
+ * ```
68
+ */
69
+ export function withBrand<B extends Brand<any, any>>(
70
+ value: B extends Brand<infer K, any> ? Omit<K, typeof __brand> : never,
71
+ ): B {
72
+ return value as B;
73
+ }
74
+
75
+ /**
76
+ * Detects if T is a union type (e.g., 'a' | 'b') vs a single literal (e.g., 'a')
77
+ */
78
+ export type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;
79
+
80
+ /**
81
+ * The Prettify helper is a utility type that takes an object type and makes the hover overlay more readable.
82
+ *
83
+ * https://www.totaltypescript.com/concepts/the-prettify-helper
84
+ */
85
+ export type Prettify<T> = {
86
+ [K in keyof T]: T[K];
87
+ } & {};
88
+
89
+ // All keys from any member of union T
90
+ type UnionKeys<T> = T extends any ? keyof T : never;
91
+
92
+ // Keys that exist in ALL members of union T
93
+ type KeysInAllMembers<T> = {
94
+ [K in UnionKeys<T>]: [T] extends [Record<K, any>] ? K : never;
95
+ }[UnionKeys<T>];
96
+
97
+ // Keys that exist in SOME but not ALL members
98
+ type KeysInSomeMembers<T> = Exclude<UnionKeys<T>, KeysInAllMembers<T>>;
99
+
100
+ // Value type for key K across all members that have it
101
+ type UnionPropValue<T, K extends PropertyKey> = T extends any
102
+ ? K extends keyof T
103
+ ? T[K]
104
+ : never
105
+ : never;
106
+
107
+ /**
108
+ * Merges a union of object types into a single object type.
109
+ * Keys present in all members become required, keys present in some become optional.
110
+ *
111
+ * ```ts
112
+ * type A = { id: string; name: string };
113
+ * type B = { id: string; age: number };
114
+ * type Merged = MergeUnion<A | B>;
115
+ * // Merged is { id: string } & { name?: string; age?: number }
116
+ * ```
117
+ */
118
+ export type MergeUnion<T> = {
119
+ [K in KeysInAllMembers<T>]: UnionPropValue<T, K>;
120
+ } & {
121
+ [K in KeysInSomeMembers<T>]?: UnionPropValue<T, K>;
122
+ };
123
+
1
124
  /**
2
125
  * An object containing a promise and its resolve/reject methods.
3
126
  */