diff --git a/index.d.ts b/index.d.ts index 8a9926d8d..b04095430 100644 --- a/index.d.ts +++ b/index.d.ts @@ -50,6 +50,7 @@ export type {SetReadonly} from './source/set-readonly.d.ts'; export type {SetRequired} from './source/set-required.d.ts'; export type {SetRequiredDeep} from './source/set-required-deep.d.ts'; export type {SetNonNullable} from './source/set-non-nullable.d.ts'; +export type {SetNullable} from './source/set-nullable.d.ts'; export type {SetNonNullableDeep} from './source/set-non-nullable-deep.d.ts'; export type {ValueOf} from './source/value-of.d.ts'; export type {AsyncReturnType} from './source/async-return-type.d.ts'; diff --git a/readme.md b/readme.md index 47e9fee30..642dd5d00 100644 --- a/readme.md +++ b/readme.md @@ -133,6 +133,7 @@ Click the type names for complete docs. - [`SetReadonly`](source/set-readonly.d.ts) - Create a type that makes the given keys readonly. - [`SetRequired`](source/set-required.d.ts) - Create a type that makes the given keys required. - [`SetRequiredDeep`](source/set-required-deep.d.ts) - Like `SetRequired` except it selects the keys deeply. +- [`SetNullable`](source/set-nullable.d.ts) - Create a type that makes the given keys nullable. - [`SetNonNullable`](source/set-non-nullable.d.ts) - Create a type that makes the given keys non-nullable. - [`SetNonNullableDeep`](source/set-non-nullable-deep.d.ts) - Create a type that makes the specified keys non-nullable (removes `null` and `undefined`), supports deeply nested key paths, and leaves all other keys unchanged. - [`ValueOf`](source/value-of.d.ts) - Create a union of the given object's values, and optionally specify which keys to get the values from. diff --git a/source/set-nullable.d.ts b/source/set-nullable.d.ts new file mode 100644 index 000000000..327c21b72 --- /dev/null +++ b/source/set-nullable.d.ts @@ -0,0 +1,35 @@ +/** +Create a type that makes the given keys nullable, where the remaining keys are kept as is. + +If no keys are given, all keys will be made nullable. + +Use-case: You want to define a single model where the only thing that changes is whether or not some or all of the keys are nullable. + +@example +``` +import type {SetNullable} from 'type-fest'; + +type Foo = { + a: number; + b: string; + c?: boolean; +}; + +type SomeNullable = SetNullable; // Optionality is preserved for `c` +//=> {a: number | null; b: string; c?: boolean | null} + +type AllNullable = SetNullable; +//=> {a: number | null; b: string | null; c?: boolean | null} +``` + +@see {@link SetNonNullable} + +@category Object +*/ +export type SetNullable = { + [Key in keyof BaseType]: Key extends Keys + ? BaseType[Key] | null + : BaseType[Key]; +}; + +export {}; diff --git a/test-d/set-nullable.ts b/test-d/set-nullable.ts new file mode 100644 index 000000000..9202885f8 --- /dev/null +++ b/test-d/set-nullable.ts @@ -0,0 +1,57 @@ +import {expectNotAssignable, expectType} from 'tsd'; +import type {SetNullable} from '../index.d.ts'; + +// Make one key nullable. +declare const variation1: SetNullable<{a: number; b: string}, 'a'>; +expectType<{a: number | null; b: string}>(variation1); + +// Make multiple keys nullable. +declare const variation2: SetNullable<{a: number; b: string; c: boolean}, 'a' | 'c'>; +expectType<{a: number | null; b: string; c: boolean | null}>(variation2); + +// Preserve optional modifier. +declare const variation3: SetNullable<{a: number; b?: string}, 'b'>; +expectType<{a: number; b?: string | null}>(variation3); + +// Key already nullable remains unchanged. +declare const variation4: SetNullable<{a: number | null; b: string}, 'a'>; +expectType<{a: number | null; b: string}>(variation4); + +// Fail if type changes even if nullable is right. +declare const variation5: SetNullable<{a: number; b: string}, 'b'>; +expectNotAssignable<{a: string; b: string | null}>(variation5); + +// Make all keys nullable when `Keys` generic is not passed. +declare const variation6: SetNullable<{a: number; b: string; c: boolean}>; +expectType<{a: number | null; b: string | null; c: boolean | null}>(variation6); + +// Preserve readonly modifier. +declare const variation7: SetNullable<{readonly a: number; b: string}, 'a'>; +expectType<{readonly a: number | null; b: string}>(variation7); + +// Works with unions. +declare const variation8: SetNullable<{a: '1'; b: string; c: boolean} | {a: '2'; b: string; c: boolean}, 'a' | 'b'>; +expectType<{a: '1' | null; b: string | null; c: boolean} | {a: '2' | null; b: string | null; c: boolean}>(variation8); + +// Works with index signatures. +declare const variation9: SetNullable<{[k: string]: unknown; a: number; b: string}, 'a'>; +expectType<{[k: string]: unknown; a: number | null; b: string}>(variation9); + +declare const variation9a: SetNullable<{[k: Uppercase]: number; A: number; b: string}, Uppercase>; +expectType<{[k: Uppercase]: number | null; A: number | null; b: string}>(variation9a); + +// Makes all keys nullable when `Keys` is `any`. +declare const variation10: SetNullable<{readonly a: number; b?: string; c: boolean}, any>; +expectType<{readonly a: number | null; b?: string | null; c: boolean | null}>(variation10); + +// Does nothing when `Keys` is `never`. +declare const variation11: SetNullable<{a: number; readonly b?: string; readonly c: boolean}, never>; +expectType<{a: number; readonly b?: string; readonly c: boolean}>(variation11); + +// Preserves existing `undefined` when adding `null`. +declare const variation12: SetNullable<{a: string | undefined; b: number}, 'a'>; +expectType<{a: string | undefined | null; b: number}>(variation12); + +// Works with unions where only some keys are shared across branches. +declare const variation13: SetNullable<{a: number; c: boolean} | {a: string; d: boolean}, 'a'>; +expectType<{a: number | null; c: boolean} | {a: string | null; d: boolean}>(variation13);