Repository navigation
New syntax: (x: X) => mutates x to Y, similar to asserts x as Y but not only narrowing #57273
Description
🔍 Search Terms
mutation, tuple push
✅ Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
Design Goals #5 might be iffy here; I suspect the questions here will be less “how does this work?” and more “is this a good idea?” I think being able to capture a common JavaScript paradigm is worth risking some surprise, but it’s a fair question.
⭐ Suggestion
A special return type similar to (x: X) => asserts x is Y, but instead is mutates x to Y. Does different (less) checking about x and Y than asserts x is Y does, and does not assume that afterwards x is X & Y, instead changing x’s type to just Y.
📃 Motivating Example
The thing that got me started on this is that we were manually creating a FormData structure (for a library):
const form = new FormData();
form.append('foo', 'something');
form.append('foo', 'else');
form.append('bar', 'Coyote Ugly');
And I wanted to type this better, so that form.getAll('foo') knows it’s getting ['something', 'else'] and form.getAll('bar') knows it’s getting ['Coyote Ugly']. I created a TypedFormData interface with a generic parameter Pairs extends Record<string, [FormDataEntryValue, ...FormDataEntryValue[]]>, and used that to type all of the methods. For mutating methods, I used asserts this is to indicate the new value.
For example, to type append (with a string value, I had overloads for blobs and blobs-with-filenames), I used
export interface TypedFormData<Pairs extends Record<string, [FormDataEntryValue, ...FormDataEntryValue[]]> = {}> extends FormData {
append<K extends string, V extends string>(name: K, value: V): asserts this is TypedFormData<{
[P in (keyof Pairs) | K]: [
...(P extends keyof Pairs ? Pairs[K] : []),
...(P extends K ? [V] : [])
] extends infer NewTuple extends [FormDataEntryValue, ...FormDataEntryValue[]] ? NewTuple : never
}>;
This actually works—once. That is, after form.append('foo', 'something') in my above, t has type TypedFormData<{ foo: ['something'] }> and all my typing works. However, further calls to append fail: I get TypedFormData<{ foo: ['something'] }> & TypedFormData<{ foo: ['something', 'else'] }> and then that does not behave correctly with any my method signatures to indicate the correct type.
This is due to how asserts works, and makes sense because asserts wasn’t meant for this scenario. But I would like something that is.
💻 Use Cases
-
Mutating objects in-place is a pretty common JavaScript pattern. There’s a trend towards immutable objects and not doing this kind of thing—which is great—but there’s a ton of existing code and libraries, including lots of the standard library, that mutate like this. There is a lot of use-case here.
-
The obvious concern is that it is arguably extremely surprising for something annotated as one thing to have a different type later. Assertions—narrowing—are one thing, since it still fits the original annotation, but this would be something actually changing and quite possibly no longer fitting that annotation at all.
Another issue is more practical: one of the biggest use-cases for this is (non-
readonly) tuples, and this suggestion alone doesn’t solve all of the problems for those. All of the mutating methods onArrayalso return values (e.g.pushreturning the new length), which is a problem here—and arguably something that also needs to be considered forasserts x is Yconstructions, because a function that asserts a type may also return things. That’s a separate concern but without addressing both, one of the strongest use-cases for this change (tuples) isn’t actually going to be solved by this change. -
The “workaround,” such as it is, is to just accept that you can’t generate an interface that will track its own type this way. You either set the type to something extremely broad and generic—losing any type safety you might have had by being more specific—or you literally just accept types that are wrong or use type casting to tell TS what is going on. We have the signatures of native methods being completely wrong in certain cases, such as the issues with mutating (non-
readonly) tuples in Bug: incorrect tuple (array) type after changing in place #52375. Per Remove destructive methods in tuple type #6325, TS isn’t going to restrict non-readonlytuples to useReadonlyArraymethods, so this is a fairly-serious instance of unsound-ness that isn’t going away.
Activity
MartinJohns commentedon Feb 2, 2024
Essentially a duplicate of #22865.
krryan commentedon Feb 2, 2024
I saw that, and it is the same concept, but a different syntax more in line with what Typescript already has (at this point).
MartinJohns commentedon Feb 2, 2024
A different syntax for the very same problem usually does not warrant a new issue. It's better suited as a comment.
Quoting RyanCavanaugh:
typescript-bot commentedon Feb 5, 2024
This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.