Repository navigation
// @ts-mutate decorator/directiveΒ #61179
Description
π Search Terms
mutate type, alter type, post-declaration narrow
β 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
β Suggestion
When doing property assignment to a variable already typed, allow a // @ts-mutate above the assignment which will
- A) Not throw an error trying to assign to a non-existent property
- B) Further narrow the type, encompassing the change
Example:
interface ObjectType {
bar: string
}
const foo: ObjectType = { bar: 'hello' }
// @ts-mutate
foo.baz = 'hello'
foo
// ^? -- this is now `ObjectType & { baz: string }`π Motivating Example
One specific example of why this would be useful would be a query constructor for type inference. There are GraphQL query constructors out there (GenQL) that query by supplying an object. I've built functions that yield reusable queries but sometimes I want to extend them.
This would allow me to take an existing query object and add an additional field to query, keeping my strict type inference.
π» Use Cases
- What do you want to use this for?
This could be used for constructing large objects sequentially and with inference without having to basically duplicate the information
- What shortcomings exist with current approaches?
If you have an existing variable, you have to alter its type declaration with the type that it is (explicitly) and intersect it with your changes. This also doesn't necessarily narrow correctly. There's no exisiting clean, easy way to do this that works off of type inference and narrowing
- What workarounds are you using in the meantime?
// UGLY const query: ReturnType<typeof queryProp> & { prop: { addition?: true } } = queryProp(...args) query.prop.addition = true
Activity
shicks commentedon Feb 14, 2025
A slightly nicer workaround (if you're using an optimizer that can inline the empty function, or else don't care about a few extra bytes) is
(playground link)
[-]`// @mutate` decorator[/-][+]`// @ts-mutate` decorator/directive[/+]snarbles2 commentedon Feb 14, 2025
I think I'd prefer a keyword or helper type, but I'd take this in any form.
This feature request is kind of a narrower version of an idea I've had the idea bouncing around my head for a while. It would be nice to have some kind of "object construction in process" type.
exoRift commentedon Feb 14, 2025
A keyword/helper type could be a nicer syntactic sugar, but that would require compiler changes and possibly break existing code. A directive would be the simplest solution for a feature that should definitely be a part of Typescript, in my opinion.
shicks commentedon Feb 14, 2025
Refining my earlier suggestion (playground).
I get that it's not particularly popular, but most optimizers can eliminate the function entirely (specifically, uglify and terser had no problem, though esbuild was less smart). The fact that it's quite doable without any new syntax suggests suggests to me that the benefit doesn't outweigh the cost.
exoRift commentedon Feb 15, 2025
What would the cost be?
I dislike your workaround because it requires runtime code for something that's purely typed. Although some bundlers optimize it out, that should not be relied on.
snarbles2 commentedon Feb 17, 2025
It requires compiler changes either way, unless you intend to pair a
//@ts-ignorewith each//@mutateor otherwise ignore the errors when your expected type mutations don't occur. Isn't new type syntax in line with how typescript has implemented these sorts of features in the past?exoRift commentedon Feb 17, 2025
In the past, yeah. Now TS is moving towards a strictly strippable instruction set like with their new flag that prevents things like enums.
I meant compiler changes as in one less thing it has to strip. A comment doesn't have any bearing on the code by default, so the only changes needed are to the type checker, not the system that transpiles.
snarbles2 commentedon Feb 17, 2025
I don't think a new helper type or type-like keyword impacts this one way or another. Type annotations are that "strippable" syntax and aren't going away, and that's where a "mutable" pseudo-type would live.
jcready commentedon Feb 18, 2025
This is where flow's comment syntax was incredibly useful. You could very well define (and "invoke") functions like the aforementioned suggestion all inside comments thus leading to zero runtime overhead while still performing the desired refinement.
shicks commentedon Feb 18, 2025
I'm not finding it anymore, but I recall seeing a post from the TypeScript maintainers that they consider new features as something like "starting at -1000" compared to not adding them, since every new feature adds both cognitive and implementation overhead and complexity.
In this case, when there's a reasonably viable workaround using existing features, that decreases the upside quite a bit.
I don't understand this argument. It seems to come from a point of view of caring so much about performance that you can't tolerate an extra trivial no-op function call (that the JIT will probably optimize away anyway) but not enough to use a proper optimizer? Which seems like it should be an empty intersection.
exoRift commentedon Mar 3, 2025
Stephen Hicks (@shicks) I actually tried your method and it doesn't work as the assertion doesn't cross the function return inference past the first depth. Technically, you could provide the path as a string and do string splitting on
.and all that dynamic jazz but then there goes any hope of inline optimizationPlayground
shicks commentedon Mar 3, 2025
If you're talking about having this work on nested properties, then it's already a whole different ballgame. For instance, ordinary
asserts arg is Talso can't handle that situation: playground. I also wouldn't expect the above-suggestedMutate<>solution to work for that, since you'd really need the property's value type to beMutaterather than expecting it to apply recursively (I can imagine plenty of cases where you don't want or expect it to act recursively). The// @ts-mutateannotation suggestion is less of a non-starter at least here, but it's a lot less appealing for other reasons. As you mentioned, you can still achieve this generally by passing an array of property names, or else with a bunch of different trivially-inlineable functions for each of the different nesting counts. But that's also getting pretty ugly.I think my point here is that there's no clear winner.
jendrikw commentedon Oct 21, 2025
I use Object.assign in these situations. Downside is that you can't reuse the same variable name.