Skip to content

Commit

Permalink
Add WritableDeep type (#540)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomerAberbach committed Feb 17, 2023
1 parent 3ebab0d commit 7538c05
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 16 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`.
- [`WritableDeep`](source/writable-deep.d.ts) - Create a deeply mutable version of an `object`/`ReadonlyMap`/`ReadonlySet`/`ReadonlyArray` type. The inverse of `ReadonlyDeep<T>`. Use `Writable<T>` 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.
Expand Down
15 changes: 15 additions & 0 deletions source/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,18 @@ type FilterOptionalKeys<T extends object> = 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<T extends (...arguments: any[]) => unknown> =
T extends {(...arguments: infer A): unknown; (...arguments: any[]): unknown}
? unknown[] extends A
? false
: true
: false;
19 changes: 3 additions & 16 deletions source/readonly-deep.d.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -66,18 +68,3 @@ Same as `ReadonlyDeep`, but accepts only `object`s as inputs. Internal helper fo
type ReadonlyObjectDeep<ObjectType extends object> = {
readonly [KeyType in keyof ObjectType]: ReadonlyDeep<ObjectType[KeyType]>
};

/**
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<T extends (...arguments: any[]) => unknown> =
T extends {(...arguments: infer A): unknown; (...arguments: any[]): unknown}
? unknown[] extends A
? false
: true
: false;
64 changes: 64 additions & 0 deletions source/writable-deep.d.ts
Original file line number Diff line number Diff line change
@@ -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<T>`. Use `Writable<T>` 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<Foo> = {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> = T extends BuiltIns
? T
: T extends (...arguments: any[]) => unknown
? {} extends WritableObjectDeep<T>
? T
: HasMultipleCallSignatures<T> extends true
? T
: ((...arguments: Parameters<T>) => ReturnType<T>) & WritableObjectDeep<T>
: T extends Readonly<ReadonlyMap<infer KeyType, infer ValueType>>
? WritableMapDeep<KeyType, ValueType>
: T extends Readonly<ReadonlySet<infer ItemType>>
? WritableSetDeep<ItemType>
: T extends object
? WritableObjectDeep<T>
: unknown;

/**
Same as `WritableDeep`, but accepts only `Map`s as inputs. Internal helper for `WritableDeep`.
*/
type WritableMapDeep<KeyType, ValueType> = {} & Writable<Map<WritableDeep<KeyType>, WritableDeep<ValueType>>>;

/**
Same as `WritableDeep`, but accepts only `Set`s as inputs. Internal helper for `WritableDeep`.
*/
type WritableSetDeep<ItemType> = {} & Writable<Set<WritableDeep<ItemType>>>;

/**
Same as `WritableDeep`, but accepts only `object`s as inputs. Internal helper for `WritableDeep`.
*/
type WritableObjectDeep<ObjectType extends object> = {
-readonly [KeyType in keyof ObjectType]: WritableDeep<ObjectType[KeyType]>
};
96 changes: 96 additions & 0 deletions test-d/writable-deep.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>(),
set: new Set<string>(),
array: ['foo'],
tuple: ['foo'] as ['foo'],
readonlyMap: new Map<string, string>() as ReadonlyMap<string, string>,
readonlySet: new Set<string>() as ReadonlySet<string>,
readonlyArray: ['foo'] as readonly string[],
readonlyTuple: ['foo'] as const,
};

const readonlyData: Readonly<typeof data> = data;

let writableData: WritableDeep<typeof readonlyData>;
expectError(writableData = readonlyData);

writableData.fn('foo');

writableData.fnWithOverload(1);
writableData.fnWithOverload('', 1);

writableData.string = 'bar';

expectType<{foo: 'bar'}>(writableData.object);
expectType<string>(writableData.string);
expectType<number>(writableData.number);
expectType<boolean>(writableData.boolean);
expectType<symbol>(writableData.symbol);
expectType<null>(writableData.null);
expectType<undefined>(writableData.undefined);
expectType<Date>(writableData.date);
expectType<RegExp>(writableData.regExp);
expectType<Writable<Map<string, string>>>(writableData.map);
expectType<Writable<Set<string>>>(writableData.set);
expectType<string[]>(writableData.array);
expectType<['foo']>(writableData.tuple);
expectType<Writable<Map<string, string>>>(writableData.readonlyMap);
expectType<Writable<Set<string>>>(writableData.readonlySet);
expectType<string[]>(writableData.readonlyArray);
expectType<['foo']>(writableData.readonlyTuple);

expectType<((foo: number) => string) & WritableObjectDeep<Namespace>>(writableData.namespace);
expectType<string>(writableData.namespace(1));
expectType<boolean[]>(writableData.namespace.baz);

// These currently aren't writable due to TypeScript limitations.
// @see https://github.com/microsoft/TypeScript/issues/29732
expectType<NamespaceWithOverload>(writableData.namespaceWithOverload);
expectType<string>(writableData.namespaceWithOverload(1));
expectType<number>(writableData.namespaceWithOverload('foo', 1));
expectType<readonly boolean[]>(writableData.namespaceWithOverload.baz);

// Test that WritableDeep is the inverse of ReadonlyDeep.
const fullyWritableData = {
array: ['a', 'b'],
map: new Map<string, number>(),
set: new Set<string>(),
object: {
date: new Date(),
boolean: true,
},
};
expectAssignable<WritableDeep<ReadonlyDeep<typeof fullyWritableData>>>(fullyWritableData);

0 comments on commit 7538c05

Please sign in to comment.