Description
Search Terms
- spread
- strict spread
There are a few related issues closed with a link to #12632 (For example #17422) but it's a lot more generic, this proposal is very specific to the spread operator where I believe there is a relatively simple fix that would handle a lot of situations.
Suggestion
Add an operator similar to the spread operator that ensure that the type of the resulting object is the same as the type of the input one.
It would serve the exact same purpose as the with
operator in F#, the |
operator in Elm or the ...
operator in ReasonML.
For example:
interface State {
readonly id: number,
readonly validated: boolean
}
// if the operator is `....` (same as ReasonML with an additional dot) :
const state: State = { id: 0, validated: false };
const newState = { ....state, validated: true }; // newState: State
const newState2 = { ....state, failed: true }; // Compilation error, 'failed' isn't in the keys of State
The following limitations would apply:
- The operator is only valid in the first position of an anonymous object build with spread
- The target type can be created by an anonymous object (so an interface not a class)
It would be compiled to ...
as the difference is only relative to typechecking
Use Cases
The central use case is to mutate objects that are used as immutable like React state or Redux state.
While it currently work most of the time as in the end the signature generated by the spread operator will be matched against the real type it produce problems:
- Errors are often shown far away from where the problem really is, requiring to hunt what function actually generated a type that had an invalid property
- Any function returning the result of a spread has a hard to read signature, often for nothing as an interface matching it exists
- The solution is often to sprinkle manual type annotations everywhere that would be unnecessary if the type wasn't lost in the first place
Examples
interface State {
readonly id: number,
readonly validated: boolean
}
// State => State
const validate = (s: State) => {....s, validated: true};
const state: State = { id: 0, validated: false };
const newState = { ....state, id: 42 }; // : State
const validatedState = validate(newState); // : State
Would also work for anonymous types :
const state = { id: 0, validated: false };
const newState = { ....state, id: 42 }; // Ok
const newState2 = { ....state, isInvalid: true }; // Compilation error, 'isInvalid' isn't in the keys of '{ id: number, validated: boolean }'
Checklist
My suggestion meets these guidelines:
- 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, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.