Skip to content

Commit

Permalink
Writable: Support array, map, and set (#726)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Emiyaaaaa and sindresorhus committed Oct 26, 2023
1 parent 964466c commit b9723d4
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 8 deletions.
2 changes: 1 addition & 1 deletion readme.md
Expand Up @@ -114,7 +114,7 @@ Click the type names for complete docs.
- [`NonEmptyObject`](source/non-empty-object.d.ts) - Represents an object with at least 1 non-optional key.
- [`UnknownRecord`](source/unknown-record.d.ts) - Represents an object with `unknown` value. You probably want this instead of `{}`.
- [`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>`.
- [`Writable`](source/writable.d.ts) - Create a type that strips `readonly` from the given type. 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.
Expand Down
42 changes: 35 additions & 7 deletions source/writable.d.ts
Expand Up @@ -2,7 +2,21 @@ import type {Except} from './except';
import type {Simplify} from './simplify';

/**
Create a type that strips `readonly` from all or some of an object's keys. Inverse of `Readonly<T>`.
Create a writable version of the given array type.
*/
type WritableArray<ArrayType extends readonly unknown[]> =
ArrayType extends readonly [] ? []
: ArrayType extends readonly [...infer U, infer V] ? [...U, V]
: ArrayType extends readonly [infer U, ...infer V] ? [U, ...V]
: ArrayType extends ReadonlyArray<infer U> ? U[]
: ArrayType;

/**
Create a type that strips `readonly` from the given type. Inverse of `Readonly<T>`.
The 2nd argument will be ignored if the input type is not an object.
Note: This type can make readonly `Set` and `Map` writable. This behavior is different from `Readonly<T>` (as of TypeScript 5.2.2). See: https://github.com/microsoft/TypeScript/issues/29655
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.
Expand All @@ -27,14 +41,28 @@ type SomeWritable = Writable<Foo, 'b' | 'c'>;
// b: readonly string[]; // It's now writable. The type of the property remains unaffected.
// c: boolean; // It's now writable.
// }
// Also supports array
const readonlyArray: readonly number[] = [1, 2, 3];
readonlyArray.push(4); // Will fail as the array itself is readonly.
const writableArray: Writable<typeof readonlyArray> = readonlyArray as Writable<typeof readonlyArray>;
writableArray.push(4); // Will work as the array itself is now writable.
```
@category Object
*/
export type Writable<BaseType, Keys extends keyof BaseType = keyof BaseType> =
Simplify<
// Pick just the keys that are not writable from the base type.
Except<BaseType, Keys> &
// Pick the keys that should be writable from the base type and make them writable by removing the `readonly` modifier from the key.
{-readonly [KeyType in keyof Pick<BaseType, Keys>]: Pick<BaseType, Keys>[KeyType]}
>;
BaseType extends ReadonlyMap<infer KeyType, infer ValueType>
? Map<KeyType, ValueType>
: BaseType extends ReadonlySet<infer ItemType>
? Set<ItemType>
: BaseType extends readonly unknown[]
// Handle array
? WritableArray<BaseType>
// Handle object
: Simplify<
// Pick just the keys that are not writable from the base type.
Except<BaseType, Keys> &
// Pick the keys that should be writable from the base type and make them writable by removing the `readonly` modifier from the key.
{-readonly [KeyType in keyof Pick<BaseType, Keys>]: Pick<BaseType, Keys>[KeyType]}
>;
22 changes: 22 additions & 0 deletions test-d/writable.ts
Expand Up @@ -26,3 +26,25 @@ expectType<{a: number; b: string; c: boolean}>(variation3);
// Check if type changes raise an error even if readonly and writable are applied correctly.
declare const variation4: Writable<{readonly a: number; b: string; readonly c: boolean}, 'b' | 'c'>;
expectNotAssignable<{readonly a: boolean; b: string; c: boolean}>(variation4);

// Test array
declare const variation5: Writable<readonly string[]>;
expectType<string[]>(variation5);

// Test tuple
declare const variation8: Writable<readonly [string, number]>;
expectType<[string, number]>(variation8);

// Test tuple with spread
declare const variation6: Writable<readonly [...string[], number]>;
expectType<[...string[], number]>(variation6);
declare const variation7: Writable<readonly [string, ...number[]]>;
expectType<[string, ...number[]]>(variation7);

// Test readonly set
declare const variation9: Writable<ReadonlySet<string>>;
expectType<Set<string>>(variation9);

// Test readonly map
declare const variation10: Writable<ReadonlyMap<string, number>>;
expectType<Map<string, number>>(variation10);

0 comments on commit b9723d4

Please sign in to comment.