From 35db7044d191bb9778fdf7446bd13253b4a34ec2 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 3 Mar 2026 23:52:50 +0700 Subject: [PATCH 1/4] Add `Optional` type Fixes #31 --- index.d.ts | 1 + readme.md | 2 ++ source/optional.d.ts | 31 +++++++++++++++++++++++++++++ test-d/optional.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 source/optional.d.ts create mode 100644 test-d/optional.ts diff --git a/index.d.ts b/index.d.ts index 52cba3c70..bba3afd97 100644 --- a/index.d.ts +++ b/index.d.ts @@ -43,6 +43,7 @@ export type {ReadonlyDeep} from './source/readonly-deep.d.ts'; export type {LiteralUnion} from './source/literal-union.d.ts'; export type {Promisable} from './source/promisable.d.ts'; export type {Arrayable} from './source/arrayable.d.ts'; +export type {Optional} from './source/optional.d.ts'; export type {Opaque, UnwrapOpaque, Tagged, GetTagMetadata, UnwrapTagged} from './source/tagged.d.ts'; export type {InvariantOf} from './source/invariant-of.d.ts'; export type {SetOptional} from './source/set-optional.d.ts'; diff --git a/readme.md b/readme.md index 46d27b5c9..305a3a546 100644 --- a/readme.md +++ b/readme.md @@ -191,6 +191,7 @@ Click the type names for complete docs. - [`ConditionalSimplify`](source/conditional-simplify.d.ts) - Simplifies a type while including and/or excluding certain types from being simplified. - [`ConditionalSimplifyDeep`](source/conditional-simplify-deep.d.ts) - Recursively simplifies a type while including and/or excluding certain types from being simplified. - [`ExclusifyUnion`](source/exclusify-union.d.ts) - Ensure mutual exclusivity in object unions by adding other members’ keys as `?: never`. +- [`Optional`](source/optional.d.ts) - Create a type that represents either the value or `undefined`, while stripping `null` from the type. ### Type Guard @@ -354,6 +355,7 @@ Click the type names for complete docs. - `PickByTypes` - See [`ConditionalPick`](source/conditional-pick.d.ts) - `HomomorphicOmit` - See [`Except`](source/except.d.ts) - `IfAny`, `IfNever`, `If*` - See [`If`](source/if.d.ts) +- `Maybe`, `Option` - See [`Optional`](source/optional.d.ts) - `MaybePromise` - See [`Promisable`](source/promisable.d.ts) - `ReadonlyTuple` - See [`TupleOf`](source/tuple-of.d.ts) diff --git a/source/optional.d.ts b/source/optional.d.ts new file mode 100644 index 000000000..81351bc9f --- /dev/null +++ b/source/optional.d.ts @@ -0,0 +1,31 @@ +/** +Create a type that represents either the value or `undefined`, while stripping `null` from the type. + +Use-cases: +- Enforcing the practice of using `undefined` instead of `null` as the "absence of value" marker. +- Converting APIs that return `null` (DOM, JSON, legacy libraries) to use `undefined` consistently. + +@example +``` +import type {Optional} from 'type-fest'; + +// Adds `undefined` to the type +type MaybeNumber = Optional; +//=> number | undefined + +// Strips `null` from the type +type NullableString = Optional; +//=> string | undefined + +type Config = { + name: string; + description: Optional; +}; +``` + +@category Utilities +*/ +// Uses `Exclude` instead of a conditional type so that `Optional` resolves to `undefined` rather than `never`. +export type Optional = Exclude | undefined; + +export {}; diff --git a/test-d/optional.ts b/test-d/optional.ts new file mode 100644 index 000000000..a008ddc63 --- /dev/null +++ b/test-d/optional.ts @@ -0,0 +1,46 @@ +import {expectType} from 'tsd'; +import type {Optional} from '../index.d.ts'; + +// Basic +expectType({} as Optional); +expectType<{foo: string} | undefined>({} as Optional<{foo: string}>); +expectType<'foo' | undefined>({} as Optional<'foo'>); +expectType<42 | undefined>({} as Optional<42>); +expectType({} as Optional); +expectType({} as Optional); +expectType<(() => void) | undefined>({} as Optional<() => void>); + +// Strips null +expectType({} as Optional); +expectType({} as Optional); +expectType({} as Optional); +expectType({} as Optional); + +// Already undefined (idempotent) +expectType({} as Optional); + +// Pure null becomes undefined (null is stripped, undefined remains) +declare const pureNull: Optional; +expectType(pureNull); + +// Null | undefined becomes undefined +declare const nullOrUndefined: Optional; +expectType(nullOrUndefined); + +// Pure undefined stays undefined +declare const pureUndefined: Optional; +expectType(pureUndefined); + +// Nested Optional is idempotent +expectType({} as Optional>); + +// Void +declare const voidOptional: Optional; +expectType(voidOptional); + +// Edge cases +expectType({} as Optional); +declare const neverOptional: Optional; +expectType(neverOptional); +// `unknown | undefined` simplifies to `unknown` +expectType({} as Optional); From 0237dee42804c48e7619885e87d8e64c797be563 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 4 Mar 2026 14:52:03 +0700 Subject: [PATCH 2/4] Update test-d/optional.ts Co-authored-by: Som Shekhar Mukherjee <49264891+som-sm@users.noreply.github.com> --- test-d/optional.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test-d/optional.ts b/test-d/optional.ts index a008ddc63..56f059eef 100644 --- a/test-d/optional.ts +++ b/test-d/optional.ts @@ -10,31 +10,31 @@ expectType({} as Optional); expectType({} as Optional); expectType<(() => void) | undefined>({} as Optional<() => void>); -// Strips null +// Strips `null` expectType({} as Optional); expectType({} as Optional); expectType({} as Optional); expectType({} as Optional); -// Already undefined (idempotent) +// Already `undefined` (idempotent) expectType({} as Optional); -// Pure null becomes undefined (null is stripped, undefined remains) +// Pure `null` becomes `undefined` (`null` is stripped, `undefined` remains) declare const pureNull: Optional; expectType(pureNull); -// Null | undefined becomes undefined +// `null | undefined` becomes `undefined` declare const nullOrUndefined: Optional; expectType(nullOrUndefined); -// Pure undefined stays undefined +// Pure `undefined` stays `undefined` declare const pureUndefined: Optional; expectType(pureUndefined); -// Nested Optional is idempotent +// Nested optional is idempotent expectType({} as Optional>); -// Void +// `void` declare const voidOptional: Optional; expectType(voidOptional); From 70cd63936db41080ef4991aba999a10f842f3bf1 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 4 Mar 2026 15:19:21 +0700 Subject: [PATCH 3/4] Update optional.ts Co-authored-by: Som Shekhar Mukherjee <49264891+som-sm@users.noreply.github.com> --- test-d/optional.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-d/optional.ts b/test-d/optional.ts index 56f059eef..02a029c04 100644 --- a/test-d/optional.ts +++ b/test-d/optional.ts @@ -31,7 +31,7 @@ expectType(nullOrUndefined); declare const pureUndefined: Optional; expectType(pureUndefined); -// Nested optional is idempotent +// Nested `Optional` is idempotent expectType({} as Optional>); // `void` From b22b550abb12525269eb13d1f5a5c0a0073f730c Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 4 Mar 2026 17:18:10 +0700 Subject: [PATCH 4/4] Update optional.d.ts --- source/optional.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/optional.d.ts b/source/optional.d.ts index 81351bc9f..8d740d8c7 100644 --- a/source/optional.d.ts +++ b/source/optional.d.ts @@ -21,11 +21,13 @@ type Config = { name: string; description: Optional; }; + +type Description = Config['description']; +//=> string | undefined ``` @category Utilities */ -// Uses `Exclude` instead of a conditional type so that `Optional` resolves to `undefined` rather than `never`. export type Optional = Exclude | undefined; export {};