diff --git a/index.d.ts b/index.d.ts index a48013f5f..5770e8731 100644 --- a/index.d.ts +++ b/index.d.ts @@ -8,6 +8,7 @@ export * from './source/observable-like'; export type {EmptyObject, IsEmptyObject} from './source/empty-object'; export type {Except} from './source/except'; export type {Writable} from './source/writable'; +export type {WritableDeep} from './source/writable-deep'; export type {Merge} from './source/merge'; export type {MergeDeep, MergeDeepOptions} from './source/merge-deep'; export type {MergeExclusive} from './source/merge-exclusive'; diff --git a/readme.md b/readme.md index 6b25615c6..245862986 100644 --- a/readme.md +++ b/readme.md @@ -129,6 +129,7 @@ Click the type names for complete docs. - [`IsEmptyObject`](source/empty-object.d.ts) - Returns a `boolean` for whether the type is strictly equal to an empty plain object, the `{}` value. - [`Except`](source/except.d.ts) - Create a type from an object type without certain keys. This is a stricter version of [`Omit`](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys). - [`Writable`](source/writable.d.ts) - Create a type that strips `readonly` from all or some of an object's keys. The inverse of `Readonly`. +- [`WritableDeep`](source/writable-deep.d.ts) - Create a deeply mutable version of an `object`/`ReadonlyMap`/`ReadonlySet`/`ReadonlyArray` type. The inverse of `ReadonlyDeep`. Use `Writable` if you only need one level deep. - [`Merge`](source/merge.d.ts) - Merge two types into a new type. Keys of the second type overrides keys of the first type. - [`MergeDeep`](source/merge-deep.d.ts) - Merge two objects or two arrays/tuples recursively into a new type. - [`MergeExclusive`](source/merge-exclusive.d.ts) - Create a type that has mutually exclusive keys. diff --git a/source/internal.d.ts b/source/internal.d.ts index 329cc9fdb..4ab9185a6 100644 --- a/source/internal.d.ts +++ b/source/internal.d.ts @@ -198,3 +198,18 @@ type FilterOptionalKeys = Exclude< }[keyof T], undefined >; + +/** +Test if the given function has multiple call signatures. + +Needed to handle the case of a single call signature with properties. + +Multiple call signatures cannot currently be supported due to a TypeScript limitation. +@see https://github.com/microsoft/TypeScript/issues/29732 +*/ +export type HasMultipleCallSignatures unknown> = + T extends {(...arguments: infer A): unknown; (...arguments: any[]): unknown} + ? unknown[] extends A + ? false + : true + : false; diff --git a/source/readonly-deep.d.ts b/source/readonly-deep.d.ts index ccbf4f6d8..cf7d52f17 100644 --- a/source/readonly-deep.d.ts +++ b/source/readonly-deep.d.ts @@ -1,4 +1,4 @@ -import type {BuiltIns} from './internal'; +import type {BuiltIns, HasMultipleCallSignatures} from './internal'; /** Convert `object`s, `Map`s, `Set`s, and `Array`s and all of their keys/elements into immutable structures recursively. @@ -29,6 +29,8 @@ data.foo.push('bar'); //=> error TS2339: Property 'push' does not exist on type 'readonly string[]' ``` +Note that types containing overloaded functions are not made deeply readonly due to a [TypeScript limitation](https://github.com/microsoft/TypeScript/issues/29732). + @category Object @category Array @category Set @@ -66,18 +68,3 @@ Same as `ReadonlyDeep`, but accepts only `object`s as inputs. Internal helper fo type ReadonlyObjectDeep = { readonly [KeyType in keyof ObjectType]: ReadonlyDeep }; - -/** -Test if the given function has multiple call signatures. - -Needed to handle the case of a single call signature with properties. - -Multiple call signatures cannot currently be supported due to a TypeScript limitation. -@see https://github.com/microsoft/TypeScript/issues/29732 -*/ -type HasMultipleCallSignatures unknown> = - T extends {(...arguments: infer A): unknown; (...arguments: any[]): unknown} - ? unknown[] extends A - ? false - : true - : false; diff --git a/source/writable-deep.d.ts b/source/writable-deep.d.ts new file mode 100644 index 000000000..1adc373a2 --- /dev/null +++ b/source/writable-deep.d.ts @@ -0,0 +1,64 @@ +import type {BuiltIns, HasMultipleCallSignatures} from './internal'; +import type {Writable} from './writable.js'; + +/** +Create a deeply mutable version of an `object`/`ReadonlyMap`/`ReadonlySet`/`ReadonlyArray` type. The inverse of `ReadonlyDeep`. Use `Writable` if you only need one level deep. + +This can be used to [store and mutate options within a class](https://github.com/sindresorhus/pageres/blob/4a5d05fca19a5fbd2f53842cbf3eb7b1b63bddd2/source/index.ts#L72), [edit `readonly` objects within tests](https://stackoverflow.com/questions/50703834), [construct a `readonly` object within a function](https://github.com/Microsoft/TypeScript/issues/24509), or to define a single model where the only thing that changes is whether or not some of the keys are writable. + +@example +``` +import type {WritableDeep} from 'type-fest'; + +type Foo = { + readonly a: number; + readonly b: readonly string[]; // To show that mutability is deeply affected. + readonly c: boolean; +}; + +const writableDeepFoo: WritableDeep = {a: 1, b: ['2'], c: true}; +writableDeepFoo.a = 3; +writableDeepFoo.b[0] = 'new value'; +writableDeepFoo.b = ['something']; +``` + +Note that types containing overloaded functions are not made deeply writable due to a [TypeScript limitation](https://github.com/microsoft/TypeScript/issues/29732). + +@see Writable +@category Object +@category Array +@category Set +@category Map +*/ +export type WritableDeep = T extends BuiltIns + ? T + : T extends (...arguments: any[]) => unknown + ? {} extends WritableObjectDeep + ? T + : HasMultipleCallSignatures extends true + ? T + : ((...arguments: Parameters) => ReturnType) & WritableObjectDeep + : T extends Readonly> + ? WritableMapDeep + : T extends Readonly> + ? WritableSetDeep + : T extends object + ? WritableObjectDeep + : unknown; + +/** +Same as `WritableDeep`, but accepts only `Map`s as inputs. Internal helper for `WritableDeep`. +*/ +type WritableMapDeep = {} & Writable, WritableDeep>>; + +/** +Same as `WritableDeep`, but accepts only `Set`s as inputs. Internal helper for `WritableDeep`. +*/ +type WritableSetDeep = {} & Writable>>; + +/** +Same as `WritableDeep`, but accepts only `object`s as inputs. Internal helper for `WritableDeep`. +*/ +type WritableObjectDeep = { + -readonly [KeyType in keyof ObjectType]: WritableDeep +}; diff --git a/test-d/writable-deep.ts b/test-d/writable-deep.ts new file mode 100644 index 000000000..f1e7ae37f --- /dev/null +++ b/test-d/writable-deep.ts @@ -0,0 +1,96 @@ +import {expectType, expectError, expectAssignable} from 'tsd'; +import type {ReadonlyDeep, Writable, WritableDeep} from '../index'; +import type {WritableObjectDeep} from '../source/writable-deep'; + +type Overloaded = { + (foo: number): string; + (foo: string, bar: number): number; +}; + +type Namespace = { + (foo: number): string; + readonly baz: readonly boolean[]; +}; + +type NamespaceWithOverload = Overloaded & { + readonly baz: readonly boolean[]; +}; + +const data = { + object: { + foo: 'bar', + } as const, + fn: (_: string) => true, + fnWithOverload: ((_: number) => 'foo') as Overloaded, + namespace: {} as unknown as Namespace, + namespaceWithOverload: {} as unknown as NamespaceWithOverload, + string: 'foo', + number: 1, + boolean: false, + symbol: Symbol('test'), + date: new Date(), + regExp: /.*/, + null: null, + undefined: undefined, // eslint-disable-line object-shorthand + map: new Map(), + set: new Set(), + array: ['foo'], + tuple: ['foo'] as ['foo'], + readonlyMap: new Map() as ReadonlyMap, + readonlySet: new Set() as ReadonlySet, + readonlyArray: ['foo'] as readonly string[], + readonlyTuple: ['foo'] as const, +}; + +const readonlyData: Readonly = data; + +let writableData: WritableDeep; +expectError(writableData = readonlyData); + +writableData.fn('foo'); + +writableData.fnWithOverload(1); +writableData.fnWithOverload('', 1); + +writableData.string = 'bar'; + +expectType<{foo: 'bar'}>(writableData.object); +expectType(writableData.string); +expectType(writableData.number); +expectType(writableData.boolean); +expectType(writableData.symbol); +expectType(writableData.null); +expectType(writableData.undefined); +expectType(writableData.date); +expectType(writableData.regExp); +expectType>>(writableData.map); +expectType>>(writableData.set); +expectType(writableData.array); +expectType<['foo']>(writableData.tuple); +expectType>>(writableData.readonlyMap); +expectType>>(writableData.readonlySet); +expectType(writableData.readonlyArray); +expectType<['foo']>(writableData.readonlyTuple); + +expectType<((foo: number) => string) & WritableObjectDeep>(writableData.namespace); +expectType(writableData.namespace(1)); +expectType(writableData.namespace.baz); + +// These currently aren't writable due to TypeScript limitations. +// @see https://github.com/microsoft/TypeScript/issues/29732 +expectType(writableData.namespaceWithOverload); +expectType(writableData.namespaceWithOverload(1)); +expectType(writableData.namespaceWithOverload('foo', 1)); +expectType(writableData.namespaceWithOverload.baz); + +// Test that WritableDeep is the inverse of ReadonlyDeep. +const fullyWritableData = { + array: ['a', 'b'], + map: new Map(), + set: new Set(), + object: { + date: new Date(), + boolean: true, + }, +}; +expectAssignable>>(fullyWritableData);