Skip to content

Writable: Fix behavior with index signatures#1470

Merged
sindresorhus merged 5 commits into
sindresorhus:mainfrom
MahinAnowar:fix/writable-preserve-structure
Jul 13, 2026
Merged

Writable: Fix behavior with index signatures#1470
sindresorhus merged 5 commits into
sindresorhus:mainfrom
MahinAnowar:fix/writable-preserve-structure

Conversation

@MahinAnowar

Copy link
Copy Markdown
Contributor

Fixes #717.

Problem

When Writable is used without the optional Keys argument, it doesn't only strip readonly — it also changes the object's structure. The clearest symptom is an index signature: Writable<{readonly [key: string]: number}> currently resolves to {[x: string]: number; [x: number]: number} — a spurious numeric index signature gets added.

This happens because the default (all-keys) case still goes through the Except<BaseType, Keys> & {-readonly [K in keyof Pick<BaseType, Keys>]: …} rebuild, and that Except/Pick round-trip does not faithfully reproduce the original shape.

Fix

As suggested in the issue thread ("add an overload when that parameter is not passed and use the simple handling"), the default case now uses a plain homomorphic mapped type:

{-readonly [KeyType in keyof BaseType]: BaseType[KeyType]}

This only removes the readonly modifier and otherwise preserves the input structure exactly, including index signatures — matching the simpler community implementations referenced in the issue. The explicit subset-of-Keys path (Writable<T, 'a' | 'b'>) is left completely unchanged, so all existing behavior and tests are preserved.

The all-keys case is detected with IsEqual<Keys, keyof BaseType> (already used internally by Except), which is true both when Keys is omitted and when the full key set is passed explicitly — semantically identical intents.

Tests

Added two tsd assertions to test-d/writable.ts:

  • Writable<{readonly [key: string]: number}>{[key: string]: number}
  • Writable<{readonly [key: string]: number; readonly foo: number}>{[key: string]: number; foo: number}

Both fail on the current code (the result carries the extra [x: number]: number index signature) and pass with the fix. Verified with git stash on the source change. The full npm test (tsc + tsd + xo + linter) passes.

When no `Keys` argument is passed, `Writable<T>` rebuilt the object via
`Except<T, keyof T> & Pick<...>`, which does not faithfully preserve the
input structure — most visibly, a `readonly` index signature came back
with a spurious numeric index signature added
(`{[x: string]: number; [x: number]: number}`).

Use a plain homomorphic mapped type for the default (all-keys) case, as
suggested by the maintainer, so only the `readonly` modifier is removed
and the shape (including index signatures) is preserved. The explicit
subset-of-`Keys` path is unchanged.

Fixes sindresorhus#717
Comment thread source/writable.d.ts Outdated
// 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]}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A better fix here would have been to change the mapped type after & to this:

& {-readonly [KeyType in keyof BaseType as KeyType extends Keys ? KeyType : never]: BaseType[KeyType]}

Passes both the newly added test cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, that's cleaner — applied in 7a49338. Both new test cases and the full suite pass with it.

@som-sm som-sm changed the title Writable: Only strip readonly without altering the structure Writable: Fix behavior with index signatures Jul 9, 2026
@sindresorhus
sindresorhus merged commit 48ddc4b into sindresorhus:main Jul 13, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Writable doesn't only strip readonly from the keys

3 participants