Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions source/writable.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {Except} from './except.d.ts';
import type {IsEqual} from './is-equal.d.ts';
import type {Simplify} from './simplify.d.ts';

/**
Expand Down Expand Up @@ -49,7 +50,7 @@ writableArray.push(4); // Will work as the array itself is now writable.

@category Object
*/
export type Writable<BaseType, Keys extends keyof BaseType = keyof BaseType> =
export type Writable<BaseType, Keys extends keyof BaseType | undefined = undefined> =
BaseType extends ReadonlyMap<infer KeyType, infer ValueType>
? Map<KeyType, ValueType>
: BaseType extends ReadonlySet<infer ItemType>
Expand All @@ -58,11 +59,13 @@ export type Writable<BaseType, Keys extends keyof BaseType = keyof BaseType> =
// Handle array
? WritableArray<BaseType>
// Handle object
: Simplify<
// Pick just the keys that are not writable from the base type.
Except<BaseType, Keys>
// Make the specified keys writable.
& {-readonly [KeyType in keyof BaseType as KeyType extends Keys ? KeyType : never]: BaseType[KeyType]}
>;
: IsEqual<Keys, undefined> extends true
? {-readonly [KeyType in keyof BaseType]: BaseType[KeyType]}
: Simplify<
// Pick just the keys that are not writable from the base type.
Except<BaseType, Extract<Keys, keyof BaseType>>
// Make the specified keys writable.
& {-readonly [KeyType in keyof BaseType as KeyType extends Keys ? KeyType : never]: BaseType[KeyType]}
>;

export {};
71 changes: 71 additions & 0 deletions test-d/writable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,74 @@ expectType<{[key: string]: number; foo: number}>(variation12);

declare const variation13: Writable<{readonly [key: string]: number; readonly foo: number}, 'foo'>;
expectType<{readonly [key: string]: number; foo: number}>(variation13);

// Support explicit concrete `keyof BaseType` as Keys argument
declare const variation14: Writable<Foo, keyof Foo>;
expectType<{a: number; b: string}>(variation14);

// A union with undefined selects only the specified keys, not all keys.
declare const variationUnionWithUndefined: Writable<Foo, 'a' | undefined>;
variationUnionWithUndefined.a = 2;
// @ts-expect-error
variationUnionWithUndefined.b = '2';
expectType<{a: number; readonly b: string}>(variationUnionWithUndefined);

// Explicit `never` makes no properties writable.
declare const variationNever: Writable<Foo, never>;
expectType<Foo>(variationNever);
// @ts-expect-error
variationNever.a = 2;
// @ts-expect-error
variationNever.b = '2';

// Computed key selection resolving to `never` makes no properties writable.
type RecordData = {readonly id: string};
type EditableKeys = Extract<keyof RecordData, `editable${string}`>;
declare const computedNeverData: Writable<RecordData, EditableKeys>;
expectType<RecordData>(computedNeverData);
// @ts-expect-error
computedNeverData.id = 'changed';

// Readonly index signature preserved when `never` is selected.
declare const indexNever: Writable<{readonly [key: string]: number}, never>;
expectType<{readonly [key: string]: number}>(indexNever);
// @ts-expect-error
indexNever.foo = 1;
// @ts-expect-error
indexNever['foo'] = 1;

// Readonly index signature preserved when computed key selection resolves to `never`.
type IndexRecord = {readonly [key: string]: number};
type IndexKeys = Extract<keyof IndexRecord, symbol>;
declare const computedIndexNever: Writable<IndexRecord, IndexKeys>;
expectType<IndexRecord>(computedIndexNever);
// @ts-expect-error
computedIndexNever.foo = 1;
// @ts-expect-error
computedIndexNever['foo'] = 1;

// Test edge cases: any, never, unknown
declare const anyVariation: Writable<any>;
expectType<any>(anyVariation);

declare const neverVariation: Writable<never>;
expectType<never>(neverVariation);

declare const unknownVariation: Writable<unknown>;
expectType<{}>(unknownVariation);

// Support polymorphic `this` within class methods (https://github.com/sindresorhus/type-fest/issues/1515).
class SomeClass {
readonly field!: number;

method() {
(this as Writable<this>).field = 4;
(this as Writable<typeof this>).field = 4;
}
}

// Support uninstantiated generic types with default Writable<T>.
function testGeneric<T extends {readonly a: number; readonly b: string}>(item: Writable<T>) {
item.a = 1;
item.b = 'test';
}
Loading