Skip to content

Commit 54bc08d

Browse files
authored
Add Option.{fromNullable, fromOptional, fromNullish} (#253)
I keep reimplementing these in projects using ts-results-es, I figured the could be added to the core. Add three static methods to the Option namespace for converting nullable, optional, and nullish values to Option<T>. fromNullable and fromOptional are the preferred APIs for T | null and T | undefined respectively as they avoid accidentally treating undefined and null the same way if the input type ever changes. fromNullish handles tT | null | undefined.
1 parent 268eb1b commit 54bc08d

6 files changed

Lines changed: 251 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ Backwards incompatible:
88

99
Added:
1010

11+
- Added `Option.fromNullable`, `Option.fromOptional`, and `Option.fromNullish`
12+
static methods for converting nullable, optional, and nullish values to
13+
`Option`.
1114
- Added array parameter overloads for `Option.all` and `Option.any`,
1215
allowing `Option.all([a, b, c])` instead of `Option.all(a, b, c)`.
1316

docs/explanation/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ Explanation
22
===========
33

44
.. toctree::
5+
nullable-optional-nullish
56
ts-results-relationship
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.. _explanation-nullable-optional-nullish:
2+
3+
Why three methods for converting nullable values to Option
4+
==========================================================
5+
6+
TypeScript distinguishes between ``null`` and ``undefined``, but it is common to treat them
7+
interchangeably using the ``T | null | undefined`` (nullish) type. This is not always correct.
8+
9+
``null`` and ``undefined`` can carry different meanings. A value that is ``null`` might mean
10+
"explicitly empty" while ``undefined`` might mean "not yet provided." If your code conflates the
11+
two, a type change from ``T | null`` to ``T | null | undefined`` — intended to introduce a new
12+
``undefined`` case that should be handled differently — will compile silently and the new case
13+
will be swallowed.
14+
15+
This is why ts-results-es provides three separate methods:
16+
17+
- :ref:`fromNullable() <method-Option-fromNullable>` for ``T | null`` — only ``null`` becomes
18+
``None``, ``undefined`` stays in ``Some``.
19+
- :ref:`fromOptional() <method-Option-fromOptional>` for ``T | undefined`` — only ``undefined``
20+
becomes ``None``, ``null`` stays in ``Some``.
21+
- :ref:`fromNullish() <method-Option-fromNullish>` for ``T | null | undefined`` — both ``null``
22+
and ``undefined`` become ``None``.
23+
24+
By using the most specific method, the compiler will catch it if the value type changes in a way
25+
that requires attention. For example, suppose you have a value of type ``string | null`` and convert
26+
it using :ref:`fromNullable() <method-Option-fromNullable>`:
27+
28+
.. code-block:: typescript
29+
30+
const name: string | null = getName();
31+
const option = Option.fromNullable(name); // Option<string>
32+
33+
If the type of ``name`` later changes to ``string | null | undefined`` — with the intention that
34+
``undefined`` should be handled differently — the code above will fail to compile, forcing you to
35+
decide how to handle the new case. Had you used
36+
:ref:`fromNullish() <method-Option-fromNullish>` instead, the change would compile silently and
37+
``undefined`` would be swallowed into ``None``.
38+
39+
Prefer :ref:`fromNullable() <method-Option-fromNullable>` or
40+
:ref:`fromOptional() <method-Option-fromOptional>` when possible. Use
41+
:ref:`fromNullish() <method-Option-fromNullish>` only when the value is already both nullable and
42+
optional and you genuinely want ``null`` and ``undefined`` to be treated the same.

docs/reference/option.rst

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,86 @@ Example:
7575
Option.any([None, None, Some(3)]); // Some(3), type: Option<number>
7676
Option.any([None, None, None]); // None, type: Option<never>
7777
78+
.. _method-Option-fromNullable:
79+
80+
``fromNullable()``
81+
------------------
82+
83+
.. code-block:: typescript
84+
85+
static fromNullable<T>(value: T): Option<Exclude<T, null>>
86+
87+
Converts a nullable value to an :ref:`Option <class-Option>`.
88+
Returns ``None`` if the value is ``null``, otherwise returns ``Some`` containing the value.
89+
90+
See also :ref:`fromOptional() <method-Option-fromOptional>` for ``T | undefined`` and :ref:`fromNullish() <method-Option-fromNullish>` for ``T | null | undefined``.
91+
92+
See also the explanation :ref:`explanation-nullable-optional-nullish`.
93+
94+
Example:
95+
96+
.. code-block:: typescript
97+
98+
const value: string | null = 'hello';
99+
Option.fromNullable(value); // Some('hello'), type: Option<string>
100+
101+
const missing: string | null = null;
102+
Option.fromNullable(missing); // None, type: Option<string>
103+
104+
.. _method-Option-fromOptional:
105+
106+
``fromOptional()``
107+
------------------
108+
109+
.. code-block:: typescript
110+
111+
static fromOptional<T>(value: T): Option<Exclude<T, undefined>>
112+
113+
Converts an optional value to an :ref:`Option <class-Option>`.
114+
Returns ``None`` if the value is ``undefined``, otherwise returns ``Some`` containing the value.
115+
116+
See also :ref:`fromNullable() <method-Option-fromNullable>` for ``T | null`` and :ref:`fromNullish() <method-Option-fromNullish>` for ``T | null | undefined``.
117+
118+
See also the explanation :ref:`explanation-nullable-optional-nullish`.
119+
120+
Example:
121+
122+
.. code-block:: typescript
123+
124+
const value: string | undefined = 'hello';
125+
Option.fromOptional(value); // Some('hello'), type: Option<string>
126+
127+
const missing: string | undefined = undefined;
128+
Option.fromOptional(missing); // None, type: Option<string>
129+
130+
.. _method-Option-fromNullish:
131+
132+
``fromNullish()``
133+
-----------------
134+
135+
.. code-block:: typescript
136+
137+
static fromNullish<T>(value: T): Option<NonNullable<T>>
138+
139+
Converts a nullish value to an :ref:`Option <class-Option>`.
140+
Returns ``None`` if the value is ``null`` or ``undefined``, otherwise returns ``Some`` containing the value.
141+
142+
Prefer :ref:`fromNullable() <method-Option-fromNullable>` for ``T | null`` or :ref:`fromOptional() <method-Option-fromOptional>` for ``T | undefined``.
143+
Use this method only when the value is already both nullable and optional and you genuinely want
144+
``null`` and ``undefined`` to be treated the same.
145+
146+
See also the explanation :ref:`explanation-nullable-optional-nullish`.
147+
148+
Example:
149+
150+
.. code-block:: typescript
151+
152+
const value: string | null | undefined = 'hello';
153+
Option.fromNullish(value); // Some('hello'), type: Option<string>
154+
155+
const missing: string | null | undefined = null;
156+
Option.fromNullish(missing); // None, type: Option<string>
157+
78158
.. _attribute-Some-EMPTY:
79159
80160
``Some.EMPTY``

src/option.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,4 +391,63 @@ export namespace Option {
391391
export function isOption<T = any>(value: unknown): value is Option<T> {
392392
return value instanceof Some || value === None;
393393
}
394+
395+
/**
396+
* Converts a nullable value to an {@link Option}.
397+
* Returns {@link None} if the value is `null`, otherwise returns {@link Some} containing the value.
398+
*
399+
* See also {@link fromOptional} for `T | undefined` and {@link fromNullish} for `T | null | undefined`.
400+
*
401+
* @example
402+
* ```typescript
403+
* const value: string | null = 'hello';
404+
* Option.fromNullable(value); // Some('hello'), type: Option<string>
405+
*
406+
* const missing: string | null = null;
407+
* Option.fromNullable(missing); // None, type: Option<string>
408+
* ```
409+
*/
410+
export function fromNullable<T>(value: T): Option<Exclude<T, null>> {
411+
return (value === null ? None : Some(value)) as Option<Exclude<T, null>>;
412+
}
413+
414+
/**
415+
* Converts an optional value to an {@link Option}.
416+
* Returns {@link None} if the value is `undefined`, otherwise returns {@link Some} containing the value.
417+
*
418+
* See also {@link fromNullable} for `T | null` and {@link fromNullish} for `T | null | undefined`.
419+
*
420+
* @example
421+
* ```typescript
422+
* const value: string | undefined = 'hello';
423+
* Option.fromOptional(value); // Some('hello'), type: Option<string>
424+
*
425+
* const missing: string | undefined = undefined;
426+
* Option.fromOptional(missing); // None, type: Option<string>
427+
* ```
428+
*/
429+
export function fromOptional<T>(value: T): Option<Exclude<T, undefined>> {
430+
return (value === undefined ? None : Some(value)) as Option<Exclude<T, undefined>>;
431+
}
432+
433+
/**
434+
* Converts a nullish value to an {@link Option}.
435+
* Returns {@link None} if the value is `null` or `undefined`, otherwise returns {@link Some} containing the value.
436+
*
437+
* Prefer {@link fromNullable} for `T | null` or {@link fromOptional} for `T | undefined`.
438+
* Use this method only when the value is already both nullable and optional and you genuinely
439+
* want `null` and `undefined` to be treated the same.
440+
*
441+
* @example
442+
* ```typescript
443+
* const value: string | null | undefined = 'hello';
444+
* Option.fromNullish(value); // Some('hello'), type: Option<string>
445+
*
446+
* const missing: string | null | undefined = null;
447+
* Option.fromNullish(missing); // None, type: Option<string>
448+
* ```
449+
*/
450+
export function fromNullish<T>(value: T): Option<NonNullable<T>> {
451+
return value === null || value === undefined ? None : Some(value);
452+
}
394453
}

test/option.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,69 @@ test('iteration', () => {
254254
expect(Array.from(Some(1))).toEqual([1]);
255255
expect(Array.from(None)).toEqual([]);
256256
});
257+
258+
test('fromNullable', () => {
259+
const value = 'hello' as string | null;
260+
const result = Option.fromNullable(value);
261+
expect(result).toEqual(Some('hello'));
262+
eq<Option<string>, typeof result>(true);
263+
264+
const missing = null as string | null;
265+
const resultMissing = Option.fromNullable(missing);
266+
expect(resultMissing).toEqual(None);
267+
eq<Option<string>, typeof resultMissing>(true);
268+
269+
// Falsy but non-null values → Some
270+
const zero = Option.fromNullable(0 as number | null);
271+
expect(zero).toEqual(Some(0));
272+
eq<Option<number>, typeof zero>(true);
273+
274+
// undefined is NOT null → Some(undefined)
275+
const undef = Option.fromNullable(undefined as undefined | null);
276+
expect(undef).toEqual(Some(undefined));
277+
eq<Option<undefined>, typeof undef>(true);
278+
});
279+
280+
test('fromOptional', () => {
281+
const value = 'hello' as string | undefined;
282+
const result = Option.fromOptional(value);
283+
expect(result).toEqual(Some('hello'));
284+
eq<Option<string>, typeof result>(true);
285+
286+
const missing = undefined as string | undefined;
287+
const resultMissing = Option.fromOptional(missing);
288+
expect(resultMissing).toEqual(None);
289+
eq<Option<string>, typeof resultMissing>(true);
290+
291+
// Falsy but non-undefined values → Some
292+
const zero = Option.fromOptional(0 as number | undefined);
293+
expect(zero).toEqual(Some(0));
294+
eq<Option<number>, typeof zero>(true);
295+
296+
// null is NOT undefined → Some(null)
297+
const nul = Option.fromOptional(null as null | undefined);
298+
expect(nul).toEqual(Some(null));
299+
eq<Option<null>, typeof nul>(true);
300+
});
301+
302+
test('fromNullish', () => {
303+
const value = 'hello' as string | null | undefined;
304+
const result = Option.fromNullish(value);
305+
expect(result).toEqual(Some('hello'));
306+
eq<Option<string>, typeof result>(true);
307+
308+
const missingNull = null as string | null | undefined;
309+
const resultNull = Option.fromNullish(missingNull);
310+
expect(resultNull).toEqual(None);
311+
eq<Option<string>, typeof resultNull>(true);
312+
313+
const missingUndefined = undefined as string | null | undefined;
314+
const resultUndefined = Option.fromNullish(missingUndefined);
315+
expect(resultUndefined).toEqual(None);
316+
eq<Option<string>, typeof resultUndefined>(true);
317+
318+
// Falsy but non-nullish values → Some
319+
const zero = Option.fromNullish(0 as number | null | undefined);
320+
expect(zero).toEqual(Some(0));
321+
eq<Option<number>, typeof zero>(true);
322+
});

0 commit comments

Comments
 (0)