ts-gems 3.12.0 → 4.0.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/README.md CHANGED
@@ -1,18 +1,130 @@
1
+ <p align="center">
2
+ <img src="docs/logo.svg" alt="ts-gems logo" width="200" height="200" />
3
+ </p>
4
+
1
5
  # ts-gems
2
6
 
3
7
  [![NPM Version][npm-image]][npm-url]
4
8
  [![NPM Downloads][downloads-image]][downloads-url]
5
9
  [![CI Tests][ci-test-image]][ci-test-url]
6
10
 
11
+ **Think of it as lodash — but every function operates on `types`, not values, and runs at compile time for free.**
12
+
13
+ Quick question: what's the type of `Partial<{ user: { name: string } }>['user']`?
14
+
15
+ If you guessed `{ name?: string }`, that's the intuitive answer — and it's wrong.
16
+ TypeScript's built-in `Partial<T>` only reaches the *first* level. The real
17
+ answer is `{ name: string }` — still fully required underneath, still ready
18
+ to blow up at runtime the moment you try to build one incrementally.
19
+
20
+ ```ts
21
+ type Config = { user: { name: string; age: number } };
22
+
23
+ type Shallow = Partial<Config>;
24
+ // { user?: { name: string; age: number } } <- still required inside!
25
+
26
+ type Deep = DeepPartial<Config>;
27
+ // { user?: { name?: string; age?: number } } <- actually usable
28
+ ```
29
+
30
+ That one gap is the whole reason this library exists. `Partial`, `Required`,
31
+ `Readonly`, and `Pick`/`Omit` all share the same blind spot: they stop at the
32
+ surface. ts-gems finishes what TypeScript started — and then keeps going into
33
+ places you didn't know needed a type yet.
34
+
35
+ ## A few things that might surprise you
36
+
37
+ **Freeze a config tree, arrays included, in one line:**
38
+
39
+ ```ts
40
+ import type { DeeperReadonly } from 'ts-gems';
41
+
42
+ type FrozenConfig = DeeperReadonly<{
43
+ servers: { host: string; port: number }[];
44
+ }>;
45
+ // every level is readonly — including inside the array elements
46
+ ```
47
+
48
+ **Turn any interface into an API-safe DTO — no functions, no symbols, recursively:**
49
+
50
+ ```ts
51
+ import type { DTO } from 'ts-gems';
52
+
53
+ class User {
54
+ name = '';
55
+ greet() {}
56
+ [Symbol.iterator]() {}
57
+ }
58
+
59
+ type UserResponse = DTO<User>;
60
+ // { name: string } — methods and symbol keys are gone, nested objects too
61
+ ```
62
+
63
+ **Stop mixing up two `number`s that were never meant to meet:**
64
+
65
+ ```ts
66
+ import type { Opaque } from 'ts-gems';
67
+
68
+ type UserId = Opaque<number, 'UserId'>;
69
+ type OrderId = Opaque<number, 'OrderId'>;
70
+
71
+ function cancelOrder(id: OrderId) {
72
+ /* ... */
73
+ }
74
+
75
+ declare const userId: UserId;
76
+ cancelOrder(userId); // ✗ compile error — nominally different, even though both are `number`
77
+ ```
78
+
79
+ **Merge types without the intersection trap:**
80
+
81
+ ```ts
82
+ type A = { name: string };
83
+ type B = { name: Date }; // overlapping key, different type
84
+
85
+ type Bad = A & B; // name: string & Date — a type nothing can satisfy. Oops.
86
+ type Good = Combine<A, B>; // { name: string } — A simply wins
87
+ ```
88
+
89
+ None of this is magic. It's ~60 small, focused utility types, each solving
90
+ one specific gap — composable, dependency-free, and fully documented with
91
+ runnable examples.
92
+
93
+ ## Explore the full toolkit
94
+
95
+ 📖 **[Browse the complete API reference →](docs/api.md)**
96
+
97
+ | Category | What it does |
98
+ | --- | --- |
99
+ | [`Deep*` / `Deeper*` family](docs/api.md#the-deep--deeper-convention) | `Mutable`, `Readonly`, `Partial`, `Required`, `Nullish` — that finally reach nested objects and arrays |
100
+ | [`DTO` / `PartialDTO` / `PatchDTO`](docs/api/dto.md) | Turn any class or interface into a clean transfer-object shape |
101
+ | [`Pick` / `Omit` family](docs/api/pick.md) | Select by key, by function-vs-data, or by matching value type |
102
+ | [`Opaque`](docs/api/opaque.md) | Nominal typing / branded primitives for TypeScript's structural type system |
103
+ | [`Combine`](docs/api/combine.md) | Merge types without the `&` intersection trap |
104
+ | [20+ type guards](docs/api/type-check.md) | `IfAny`, `IfNever`, `IfEquals`, `IfTuple`, `IfCompatible`, and more, for building your own conditional types |
105
+ | [`And` / `Or`](docs/api/logical.md) | Compile-time boolean logic to combine several guards into one |
106
+
7
107
  ## Installation
8
108
 
9
109
  ```bash
10
- $ npm install ts-gems --save
110
+ npm install ts-gems --save
111
+ ```
112
+
113
+ ```ts
114
+ import { DeepPartial, DTO, Opaque, StrictOmit } from 'ts-gems';
11
115
  ```
116
+
117
+ Everything is exported from the package root — no sub-path imports, no
118
+ runtime cost. It's types all the way down (with twelve tiny `as*` cast
119
+ helpers thrown in, purely for ergonomics).
120
+
12
121
  ## Node Compatibility
122
+
13
123
  - node >= 16.x
124
+
14
125
  ## License
15
- ts-gems is available under [MIT](LICENSE) license.
126
+
127
+ ts-gems is available under the [MIT](LICENSE) license.
16
128
 
17
129
  [npm-image]: https://img.shields.io/npm/v/ts-gems
18
130
  [npm-url]: https://npmjs.org/package/ts-gems
@@ -0,0 +1,48 @@
1
+ # Combine
2
+
3
+ Source: [`lib/combine.d.ts`](../../lib/combine.d.ts)
4
+
5
+ ## `Combine<T1, T2, T3 = {}, T4 = {}>`
6
+
7
+ ```ts
8
+ type Combine<T1, T2, T3 = {}, T4 = {}> = T1 &
9
+ Omit<T2, keyof T1> &
10
+ Omit<T3, keyof T1 | keyof T2> &
11
+ Omit<T4, keyof T1 | keyof T2 | keyof T3>;
12
+ ```
13
+
14
+ Merges up to four object types into one, **without** merging the types of
15
+ overlapping properties — unlike a plain intersection (`T1 & T2`), which
16
+ would combine both types of a shared key into `T1[K] & T2[K]`, `Combine`
17
+ lets the earliest argument win outright for any key present in more than
18
+ one input.
19
+
20
+ ```ts
21
+ import type { Combine } from 'ts-gems';
22
+
23
+ interface Base {
24
+ id: string;
25
+ name: string;
26
+ }
27
+
28
+ interface Timestamps {
29
+ name: Date; // overlaps with `Base.name` - Base wins
30
+ createdAt: Date;
31
+ }
32
+
33
+ type Entity = Combine<Base, Timestamps>;
34
+ // { id: string; name: string; createdAt: Date }
35
+ // (compare to `Base & Timestamps`, where `name` would be `string & Date` = `never`)
36
+ ```
37
+
38
+ Precedence follows argument order — `T1` wins over `T2`, which wins over
39
+ `T3`, which wins over `T4`:
40
+
41
+ ```ts
42
+ type A = { x: 1 };
43
+ type B = { x: 2; y: 2 };
44
+ type C = { x: 3; y: 3; z: 3 };
45
+
46
+ type Result = Combine<A, B, C>;
47
+ // { x: 1; y: 2; z: 3 }
48
+ ```
@@ -0,0 +1,115 @@
1
+ # DTO
2
+
3
+ Source: [`lib/dto.d.ts`](../../lib/dto.d.ts)
4
+
5
+ Turns any type into a plain Data Transfer Object shape: strips function
6
+ properties and symbol-keyed properties, and always deep-processes nested
7
+ objects and arrays (there's no shallow/`Deep` variant — `DTO` behaves like
8
+ the `Deeper*` members of [the convention](../api.md#the-deep--deeper-convention)).
9
+ Tuples are left untouched, same as everywhere else in the library.
10
+
11
+ ## `DTO<T, X = never>`
12
+
13
+ ```ts
14
+ import type { DTO } from 'ts-gems';
15
+
16
+ interface User {
17
+ name: string;
18
+ age?: number;
19
+ greet(): void; // function - removed
20
+ [Symbol.iterator]: () => Iterator<unknown>; // symbol key - removed
21
+ }
22
+
23
+ type UserDTO = DTO<User>;
24
+ // { name: string; age?: number }
25
+ ```
26
+
27
+ Nested objects and arrays are always processed, recursively:
28
+
29
+ ```ts
30
+ interface Order {
31
+ id: string;
32
+ createdBy: { name: string; greet(): void };
33
+ items: { sku: string; validate(): boolean }[];
34
+ }
35
+
36
+ type OrderDTO = DTO<Order>;
37
+ // {
38
+ // id: string;
39
+ // createdBy: { name: string };
40
+ // items: { sku: string }[];
41
+ // }
42
+ ```
43
+
44
+ ### The `X` parameter
45
+
46
+ The second type parameter, `X`, is unioned into **every** remaining
47
+ property's value type (through `NonNullable<T[K] | X>`, applied at every
48
+ nesting level). This is handy for tagging every field of the DTO with a
49
+ shared extra type — for example, a sentinel used to represent "field
50
+ explicitly not selected" in a partial-response GraphQL/RPC layer:
51
+
52
+ ```ts
53
+ import type { DTO } from 'ts-gems';
54
+
55
+ const NOT_SELECTED = Symbol('not-selected');
56
+ type NotSelected = typeof NOT_SELECTED;
57
+
58
+ interface User {
59
+ name: string;
60
+ age?: number;
61
+ }
62
+
63
+ type SparseUserDTO = DTO<User, NotSelected>;
64
+ // { name: string | NotSelected; age?: number | NotSelected }
65
+ ```
66
+
67
+ > Because `X` is combined via `NonNullable<T[K] | X>`, passing `null` or
68
+ > `undefined` as `X` has no visible effect — both are immediately stripped
69
+ > again. Use a non-nullish sentinel (a literal, a branded/[`Opaque`](opaque.md)
70
+ > type, or a `unique symbol` as above) if you need one.
71
+
72
+ ## `PartialDTO<T, X = never>`
73
+
74
+ ```ts
75
+ type PartialDTO<T, X = never> = DeeperPartial<DTO<T, X>>;
76
+ ```
77
+
78
+ `DTO<T, X>` with every property (including nested ones and array elements)
79
+ made optional via [`DeeperPartial`](partial.md#deeperpartialt) — a common
80
+ shape for "create" endpoint request bodies where every field may be omitted.
81
+
82
+ ```ts
83
+ import type { PartialDTO } from 'ts-gems';
84
+
85
+ interface User {
86
+ name: string;
87
+ profile: { bio: string };
88
+ }
89
+
90
+ type CreateUserBody = PartialDTO<User>;
91
+ // { name?: string; profile?: { bio?: string } }
92
+ ```
93
+
94
+ ## `PatchDTO<T, X = never>`
95
+
96
+ ```ts
97
+ type PatchDTO<T, X = never> = DeeperNullish<DTO<T, X>>;
98
+ ```
99
+
100
+ `DTO<T, X>` with every property (including nested ones and array elements)
101
+ made optional **and** nullable via [`DeeperNullish`](nullish.md#deepernullisht)
102
+ — a common shape for "patch"/"update" endpoint request bodies, where `null`
103
+ means "clear this field" and omitting the key means "leave it unchanged".
104
+
105
+ ```ts
106
+ import type { PatchDTO } from 'ts-gems';
107
+
108
+ interface User {
109
+ name: string;
110
+ profile: { bio: string };
111
+ }
112
+
113
+ type UpdateUserBody = PatchDTO<User>;
114
+ // { name?: string | null; profile?: { bio?: string | null } | null }
115
+ ```
@@ -0,0 +1,97 @@
1
+ # Helpers
2
+
3
+ Source: [`lib/helpers.d.ts`](../../lib/helpers.d.ts)
4
+
5
+ ## `IfNoDeepValue<T>`
6
+
7
+ ```ts
8
+ type IfNoDeepValue<T> = /* ... */; // true | false
9
+ ```
10
+
11
+ `true` when `T` should be treated as a **leaf** by every `Deep*`/`Deeper*`
12
+ transform in this library — i.e. left untouched instead of being recursed
13
+ into. This is the type that defines "leaf" for [the `Deep*`/`Deeper*`
14
+ convention](../api.md#the-deep--deeper-convention): `true` for `any`, every
15
+ [`Builtin`](types.md#builtin) type, a [tuple](type-check.md#iftuplet-y-n), a
16
+ `Function`, a class/constructor reference, `Map`/`ReadonlyMap`/`WeakMap`/
17
+ `Set`/`ReadonlySet`/`WeakSet`, or any array; `false` for a plain object.
18
+
19
+ ```ts
20
+ import type { IfNoDeepValue } from 'ts-gems';
21
+
22
+ type A = IfNoDeepValue<string>; // true
23
+ type B = IfNoDeepValue<any>; // true
24
+ type C = IfNoDeepValue<[string, number]>; // true - a tuple
25
+ type D = IfNoDeepValue<Date>; // true
26
+ type E = IfNoDeepValue<Map<string, number>>; // true
27
+ type F = IfNoDeepValue<{ a: string }>; // false - a plain object, gets recursed into
28
+ ```
29
+
30
+ You'll rarely need this directly unless you're writing your own `Deep*`-style
31
+ recursive type and want it to treat leaves consistently with the rest of the
32
+ library.
33
+
34
+ ## `ValuesOf<T>`
35
+
36
+ ```ts
37
+ type ValuesOf<T> = T[keyof T];
38
+ ```
39
+
40
+ Returns the union of every property value type in `T` — the value-side
41
+ equivalent of `keyof`.
42
+
43
+ ```ts
44
+ import type { ValuesOf } from 'ts-gems';
45
+
46
+ interface Row {
47
+ a: number;
48
+ b: string;
49
+ c?: boolean;
50
+ }
51
+
52
+ type Values = ValuesOf<Row>; // number | string | boolean | undefined
53
+ ```
54
+
55
+ ## Runtime cast helpers
56
+
57
+ Twelve tiny runtime functions, declared in
58
+ [`lib/index.d.ts`](../../lib/index.d.ts) and implemented as a no-op identity
59
+ function at runtime (`lib/index.js`). They exist purely so you can _cast_ a
60
+ value's static type without an `as` expression or reaching for a type
61
+ import at the call site:
62
+
63
+ ```ts
64
+ declare function asMutable<T>(x: T): Mutable<T>;
65
+ declare function asDeepMutable<T>(x: T): DeepMutable<T>;
66
+ declare function asDeeperMutable<T>(x: T): DeeperMutable<T>;
67
+
68
+ declare function asReadonly<T>(x: T): Readonly<T>;
69
+ declare function asDeepReadonly<T>(x: T): DeepReadonly<T>;
70
+ declare function asDeeperReadonly<T>(x: T): DeeperReadonly<T>;
71
+
72
+ declare function asPartial<T>(x: T): Partial<T>;
73
+ declare function asDeepPartial<T>(x: T): DeepPartial<T>;
74
+ declare function asDeeperPartial<T>(x: T): DeeperPartial<T>;
75
+
76
+ declare function asRequired<T>(x: T): Required<T>;
77
+ declare function asDeepRequired<T>(x: T): DeepRequired<T>;
78
+ declare function asDeeperRequired<T>(x: T): DeeperRequired<T>;
79
+ ```
80
+
81
+ ```ts
82
+ import { asMutable } from 'ts-gems';
83
+
84
+ interface Point {
85
+ readonly x: number;
86
+ readonly y: number;
87
+ }
88
+
89
+ function move(p: Point) {
90
+ const mutablePoint = asMutable(p); // same object reference, `Mutable<Point>` type
91
+ mutablePoint.x += 1; // ok - no cast needed, and no runtime cost
92
+ }
93
+ ```
94
+
95
+ Because these are real functions (not just types), they also work well with
96
+ plain JavaScript editors/tools that only understand runtime code — the
97
+ identity behavior at runtime means they're always safe to call.
@@ -0,0 +1,48 @@
1
+ # Logical
2
+
3
+ Source: [`lib/logical.d.ts`](../../lib/logical.d.ts)
4
+
5
+ Compile-time boolean logic over up to six type-level "truthy" values, useful
6
+ when composing several [type guards](type-check.md) into one condition. A
7
+ value counts as "falsy" when it is `undefined`, `null`, or the literal type
8
+ `false`; anything else (including `never`... see below) counts as "truthy".
9
+
10
+ Unused trailing parameters default to the operator's identity value (`true`
11
+ for `And`, `false` for `Or`), so you can pass anywhere from 2 to 6 arguments.
12
+
13
+ ## `And<T1, T2, T3?, T4?, T5?, T6?>`
14
+
15
+ `true` only if every provided argument is truthy.
16
+
17
+ ```ts
18
+ import type { And } from 'ts-gems';
19
+
20
+ type A = And<true, true>; // true
21
+ type B = And<true, false>; // false
22
+ type C = And<true, true, true, true, true, true>; // true
23
+ type D = And<true, never>; // false - `never` counts as falsy here
24
+ ```
25
+
26
+ A common use is combining several `If*` guards:
27
+
28
+ ```ts
29
+ import type { And, IfObject, IfNever } from 'ts-gems';
30
+
31
+ type IsPlainRequiredObject<T> = And<IfObject<T>, IfNever<T, false, true>>;
32
+ ```
33
+
34
+ ## `Or<T1, T2, T3?, T4?, T5?, T6?>`
35
+
36
+ `true` if **any** provided argument is truthy.
37
+
38
+ ```ts
39
+ import type { Or } from 'ts-gems';
40
+
41
+ type A = Or<false, false>; // false
42
+ type B = Or<false, true>; // true
43
+ type C = Or<false, false, false, false, false, true>; // true
44
+ ```
45
+
46
+ This is the exact building block the [`Pick`](pick.md)/[`Omit`](omit.md)
47
+ family uses internally to combine several exclusion criteria into one key
48
+ filter — e.g. "omit this key if it's `never` **or** if it's a function".
@@ -0,0 +1,164 @@
1
+ # Mutable
2
+
3
+ Source: [`lib/mutable.d.ts`](../../lib/mutable.d.ts)
4
+
5
+ Strips `readonly` modifiers. See [the `Deep*`/`Deeper*` convention](../api.md#the-deep--deeper-convention)
6
+ for what "deeply" means here, and the runtime [`asMutable` cast
7
+ helpers](helpers.md#runtime-cast-helpers).
8
+
9
+ ## `Mutable<T>`
10
+
11
+ Removes `readonly` from every top-level property. Properties whose value is
12
+ `never` (after stripping `undefined`) are dropped, matching the rest of the
13
+ library's "never means absent" convention.
14
+
15
+ ```ts
16
+ import type { Mutable } from 'ts-gems';
17
+
18
+ interface Point {
19
+ readonly x: number;
20
+ readonly y: number;
21
+ }
22
+
23
+ type MutablePoint = Mutable<Point>;
24
+ // { x: number; y: number }
25
+
26
+ const p: MutablePoint = { x: 1, y: 2 };
27
+ p.x = 3; // ok - no longer readonly
28
+ ```
29
+
30
+ ## `MutableSome<T, K>`
31
+
32
+ Removes `readonly` from only the properties named in `K`; every other
33
+ property is untouched.
34
+
35
+ ```ts
36
+ import type { MutableSome } from 'ts-gems';
37
+
38
+ interface Row {
39
+ readonly id: number;
40
+ readonly name: string;
41
+ readonly createdAt: Date;
42
+ }
43
+
44
+ type EditableRow = MutableSome<Row, 'name'>;
45
+ // { readonly id: number; name: string; readonly createdAt: Date }
46
+ ```
47
+
48
+ ## `DeepMutable<T>`
49
+
50
+ Like `Mutable`, but also strips `readonly` from nested object properties.
51
+ Array-typed properties are left exactly as they are (see the convention
52
+ table) — use `DeeperMutable` to also transform array element types.
53
+
54
+ ```ts
55
+ import type { DeepMutable } from 'ts-gems';
56
+
57
+ interface Config {
58
+ readonly name: string;
59
+ readonly server: {
60
+ readonly host: string;
61
+ readonly port: number;
62
+ };
63
+ readonly tags: string[];
64
+ }
65
+
66
+ type MutableConfig = DeepMutable<Config>;
67
+ // {
68
+ // name: string;
69
+ // server: { host: string; port: number };
70
+ // tags: string[]; // untouched - it's an array
71
+ // }
72
+ ```
73
+
74
+ ## `DeeperMutable<T>`
75
+
76
+ Like `DeepMutable`, but also makes the element type of array-typed
77
+ properties mutable. Tuples are left untouched (their fixed shape is
78
+ preserved as-is).
79
+
80
+ ```ts
81
+ import type { DeeperMutable } from 'ts-gems';
82
+
83
+ interface Config {
84
+ readonly server: { readonly host: string };
85
+ readonly servers: { readonly host: string }[];
86
+ readonly range: readonly [number, number]; // tuple
87
+ }
88
+
89
+ type MutableConfig = DeeperMutable<Config>;
90
+ // {
91
+ // server: { host: string };
92
+ // servers: { host: string }[]; // element type made mutable too
93
+ // range: readonly [number, number]; // tuple - unchanged
94
+ // }
95
+ ```
96
+
97
+ ## `MutableKeys<T>`
98
+
99
+ Returns the union of property names in `T` that are **not** `readonly`.
100
+
101
+ ```ts
102
+ import type { MutableKeys } from 'ts-gems';
103
+
104
+ interface Row {
105
+ readonly id: number;
106
+ name: string;
107
+ }
108
+
109
+ type Editable = MutableKeys<Row>; // 'name'
110
+ ```
111
+
112
+ ## `PickMutable<T>` / `OmitMutable<T>`
113
+
114
+ Pick (or omit) only the properties that are **not** `readonly`. These are
115
+ implemented in terms of [`OmitReadonly`/`PickReadonly`](readonly.md), since
116
+ "mutable" and "readonly" partition every property into exactly two sets.
117
+
118
+ ```ts
119
+ import type { OmitMutable, PickMutable } from 'ts-gems';
120
+
121
+ interface Row {
122
+ readonly id: number;
123
+ name: string;
124
+ }
125
+
126
+ type OnlyMutable = PickMutable<Row>; // { name: string }
127
+ type OnlyReadonly = OmitMutable<Row>; // { readonly id: number }
128
+ ```
129
+
130
+ ## `DeepPickMutable<T>` / `DeepOmitMutable<T>`
131
+
132
+ The deep versions of `PickMutable`/`OmitMutable` — nested objects are
133
+ filtered the same way, recursively. Delegates to
134
+ [`DeepOmitReadonly`/`DeepPickReadonly`](readonly.md).
135
+
136
+ ```ts
137
+ import type { DeepPickMutable } from 'ts-gems';
138
+
139
+ interface Row {
140
+ readonly id: number;
141
+ meta: { readonly createdBy: string; label: string };
142
+ }
143
+
144
+ type Editable = DeepPickMutable<Row>;
145
+ // { meta: { label: string } }
146
+ ```
147
+
148
+ ## `DeeperPickMutable<T>` / `DeeperOmitMutable<T>`
149
+
150
+ Like `DeepPickMutable`/`DeepOmitMutable`, but also recurses into array
151
+ elements. Delegates to
152
+ [`DeeperOmitReadonly`/`DeeperPickReadonly`](readonly.md).
153
+
154
+ ```ts
155
+ import type { DeeperPickMutable } from 'ts-gems';
156
+
157
+ interface Row {
158
+ readonly id: number;
159
+ tags: { readonly createdBy: string; label: string }[];
160
+ }
161
+
162
+ type Editable = DeeperPickMutable<Row>;
163
+ // { tags: { label: string }[] }
164
+ ```
@@ -0,0 +1,68 @@
1
+ # UnNullish
2
+
3
+ Source: [`lib/non-nullable.d.ts`](../../lib/non-nullable.d.ts)
4
+
5
+ Removes `null` and `undefined` from every property's value type (via
6
+ `NonNullable`), and drops the key entirely if nothing is left afterwards.
7
+ See [the `Deep*`/`Deeper*` convention](../api.md#the-deep--deeper-convention).
8
+
9
+ ## `UnNullish<T>`
10
+
11
+ **Shallow** — only top-level properties are un-nullished. Nested `null`/
12
+ `undefined` values, inside an object or array property, are left exactly as
13
+ they are. Use [`DeepUnNullish`](#deepunnullisht) or
14
+ [`DeeperUnNullish`](#deeperunnullisht) to reach into nested structures too.
15
+
16
+ ```ts
17
+ import type { UnNullish } from 'ts-gems';
18
+
19
+ type MyType = {
20
+ a: string | null;
21
+ b?: number | null;
22
+ nested: { c: string | null } | null;
23
+ };
24
+
25
+ type Result = UnNullish<MyType>;
26
+ // {
27
+ // a: string;
28
+ // b?: number;
29
+ // nested: { c: string | null }; // the outer `| null` is gone, the inner one isn't
30
+ // }
31
+ ```
32
+
33
+ ## `DeepUnNullish<T>`
34
+
35
+ Like `UnNullish`, but also un-nullishes nested object properties,
36
+ recursively. Array-typed properties are left as-is.
37
+
38
+ ```ts
39
+ import type { DeepUnNullish } from 'ts-gems';
40
+
41
+ type MyType = {
42
+ nested: { c: string | null } | null;
43
+ list: { c: string | null }[] | null;
44
+ };
45
+
46
+ type Result = DeepUnNullish<MyType>;
47
+ // {
48
+ // nested: { c: string };
49
+ // list: { c: string | null }[]; // untouched - it's an array
50
+ // }
51
+ ```
52
+
53
+ ## `DeeperUnNullish<T>`
54
+
55
+ Like `DeepUnNullish`, but also recurses into array elements. Tuples are
56
+ preserved as-is (their own `| null` is still stripped).
57
+
58
+ ```ts
59
+ import type { DeeperUnNullish } from 'ts-gems';
60
+
61
+ type MyType = {
62
+ list: { c: string | null }[] | null;
63
+ pair: [string, number] | null; // tuple
64
+ };
65
+
66
+ type Result = DeeperUnNullish<MyType>;
67
+ // { list: { c: string }[]; pair: [string, number] } - tuple untouched
68
+ ```