From 08b29d21cf4391350dd73f0c94bc1f0b4927c582 Mon Sep 17 00:00:00 2001 From: Deshraj Verma Date: Sun, 6 Sep 2026 13:40:43 +0530 Subject: [PATCH 1/3] `Writable`: Fix support for polymorphic `this` and generics (#1515) --- source/writable.d.ts | 20 +++++++++++++------- test-d/writable.ts | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/source/writable.d.ts b/source/writable.d.ts index 625071e93..e3f0fcc98 100644 --- a/source/writable.d.ts +++ b/source/writable.d.ts @@ -1,4 +1,6 @@ import type {Except} from './except.d.ts'; +import type {IsEqual} from './is-equal.d.ts'; +import type {IsNever} from './is-never.d.ts'; import type {Simplify} from './simplify.d.ts'; /** @@ -49,7 +51,7 @@ writableArray.push(4); // Will work as the array itself is now writable. @category Object */ -export type Writable = +export type Writable = BaseType extends ReadonlyMap ? Map : BaseType extends ReadonlySet @@ -58,11 +60,15 @@ export type Writable = // Handle array ? WritableArray // Handle object - : Simplify< - // Pick just the keys that are not writable from the base type. - Except - // Make the specified keys writable. - & {-readonly [KeyType in keyof BaseType as KeyType extends Keys ? KeyType : never]: BaseType[KeyType]} - >; + : IsNever extends true + ? {-readonly [KeyType in keyof BaseType]: BaseType[KeyType]} + : IsEqual extends true + ? {-readonly [KeyType in keyof BaseType]: BaseType[KeyType]} + : Simplify< + // Pick just the keys that are not writable from the base type. + Except + // Make the specified keys writable. + & {-readonly [KeyType in keyof BaseType as KeyType extends Keys ? KeyType : never]: BaseType[KeyType]} + >; export {}; diff --git a/test-d/writable.ts b/test-d/writable.ts index 8154a646b..4058576bb 100644 --- a/test-d/writable.ts +++ b/test-d/writable.ts @@ -59,3 +59,27 @@ 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 `keyof BaseType` as Keys argument +declare const variation14: Writable; +expectType<{a: number; b: string}>(variation14); + +// Test edge cases: any, never, unknown +declare const anyVariation: Writable; +expectType(anyVariation); + +declare const neverVariation: Writable; +expectType(neverVariation); + +declare const unknownVariation: Writable; +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).field = 4; + (this as Writable).field = 4; + } +} From ec31894cc2f4495a8f44988b2a9588da758b7d6b Mon Sep 17 00:00:00 2001 From: Deshraj Verma Date: Sun, 6 Sep 2026 18:02:13 +0530 Subject: [PATCH 2/3] `Writable`: Distinguish omitted Keys from empty selections (#1515) --- source/writable.d.ts | 9 ++++----- test-d/writable.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/source/writable.d.ts b/source/writable.d.ts index e3f0fcc98..3fb4fa00b 100644 --- a/source/writable.d.ts +++ b/source/writable.d.ts @@ -1,6 +1,5 @@ import type {Except} from './except.d.ts'; import type {IsEqual} from './is-equal.d.ts'; -import type {IsNever} from './is-never.d.ts'; import type {Simplify} from './simplify.d.ts'; /** @@ -51,7 +50,7 @@ writableArray.push(4); // Will work as the array itself is now writable. @category Object */ -export type Writable = +export type Writable = BaseType extends ReadonlyMap ? Map : BaseType extends ReadonlySet @@ -60,13 +59,13 @@ export type Writable = // Handle array ? WritableArray // Handle object - : IsNever extends true + : [undefined] extends [Keys] ? {-readonly [KeyType in keyof BaseType]: BaseType[KeyType]} - : IsEqual extends true + : IsEqual, keyof BaseType> extends true ? {-readonly [KeyType in keyof BaseType]: BaseType[KeyType]} : Simplify< // Pick just the keys that are not writable from the base type. - Except + Except> // Make the specified keys writable. & {-readonly [KeyType in keyof BaseType as KeyType extends Keys ? KeyType : never]: BaseType[KeyType]} >; diff --git a/test-d/writable.ts b/test-d/writable.ts index 4058576bb..3bfe22ded 100644 --- a/test-d/writable.ts +++ b/test-d/writable.ts @@ -64,6 +64,40 @@ expectType<{readonly [key: string]: number; foo: number}>(variation13); declare const variation14: Writable; expectType<{a: number; b: string}>(variation14); +// Explicit `never` makes no properties writable. +declare const variationNever: Writable; +expectType(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; +declare const computedNeverData: Writable; +expectType(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; +declare const computedIndexNever: Writable; +expectType(computedIndexNever); +// @ts-expect-error +computedIndexNever.foo = 1; +// @ts-expect-error +computedIndexNever['foo'] = 1; + // Test edge cases: any, never, unknown declare const anyVariation: Writable; expectType(anyVariation); From a7ec8550c5989649bb1b3fd8d328663e18bb330d Mon Sep 17 00:00:00 2001 From: Deshraj Verma Date: Mon, 7 Sep 2026 09:53:26 +0530 Subject: [PATCH 3/3] Writable: Use IsEqual for undefined sentinel and clean up generic keyof (#1515) --- source/writable.d.ts | 16 +++++++--------- test-d/writable.ts | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/source/writable.d.ts b/source/writable.d.ts index 3fb4fa00b..c85459a4f 100644 --- a/source/writable.d.ts +++ b/source/writable.d.ts @@ -59,15 +59,13 @@ export type Writable // Handle object - : [undefined] extends [Keys] + : IsEqual extends true ? {-readonly [KeyType in keyof BaseType]: BaseType[KeyType]} - : IsEqual, keyof BaseType> extends true - ? {-readonly [KeyType in keyof BaseType]: BaseType[KeyType]} - : Simplify< - // Pick just the keys that are not writable from the base type. - Except> - // Make the specified keys writable. - & {-readonly [KeyType in keyof BaseType as KeyType extends Keys ? KeyType : never]: BaseType[KeyType]} - >; + : Simplify< + // Pick just the keys that are not writable from the base type. + Except> + // Make the specified keys writable. + & {-readonly [KeyType in keyof BaseType as KeyType extends Keys ? KeyType : never]: BaseType[KeyType]} + >; export {}; diff --git a/test-d/writable.ts b/test-d/writable.ts index 3bfe22ded..974a4a9da 100644 --- a/test-d/writable.ts +++ b/test-d/writable.ts @@ -60,10 +60,17 @@ 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 `keyof BaseType` as Keys argument +// Support explicit concrete `keyof BaseType` as Keys argument declare const variation14: Writable; expectType<{a: number; b: string}>(variation14); +// A union with undefined selects only the specified keys, not all keys. +declare const variationUnionWithUndefined: Writable; +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; expectType(variationNever); @@ -90,7 +97,7 @@ indexNever['foo'] = 1; // Readonly index signature preserved when computed key selection resolves to `never`. type IndexRecord = {readonly [key: string]: number}; -type IndexKeys = Extract; +type IndexKeys = Extract; declare const computedIndexNever: Writable; expectType(computedIndexNever); // @ts-expect-error @@ -117,3 +124,9 @@ class SomeClass { (this as Writable).field = 4; } } + +// Support uninstantiated generic types with default Writable. +function testGeneric(item: Writable) { + item.a = 1; + item.b = 'test'; +}