New syntax: (x: X) => mutates x to Y, similar to asserts x as Y but not only narrowing #57273
Closed
6 tasks done
Labels
Duplicate
An existing issue was already created
🔍 Search Terms
mutation, tuple push
✅ Viability Checklist
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 ismutates x to Y
. Does different (less) checking aboutx
andY
thanasserts x is Y
does, and does not assume that afterwardsx
isX & Y
, instead changingx
’s type to justY
.📃 Motivating Example
The thing that got me started on this is that we were manually creating a
FormData
structure (for a library):And I wanted to type this better, so that
form.getAll('foo')
knows it’s getting['something', 'else']
andform.getAll('bar')
knows it’s getting['Coyote Ugly']
. I created aTypedFormData
interface with a generic parameterPairs extends Record<string, [FormDataEntryValue, ...FormDataEntryValue[]]>
, and used that to type all of the methods. For mutating methods, I usedasserts 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 usedThis actually works—once. That is, after
form.append('foo', 'something')
in my above,t
has typeTypedFormData<{ foo: ['something'] }>
and all my typing works. However, further calls toappend
fail: I getTypedFormData<{ 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 becauseasserts
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 onArray
also return values (e.g.push
returning the new length), which is a problem here—and arguably something that also needs to be considered forasserts x is Y
constructions, 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-readonly
tuples to useReadonlyArray
methods, so this is a fairly-serious instance of unsound-ness that isn’t going away.The text was updated successfully, but these errors were encountered: