Description
Suggestion
🔍 Search Terms
List of keywords you searched for before creating this issue. Write them down here so that others can find this suggestion more easily and help provide feedback.
dependent arguments, parameters
This issue is very related to mine here, but I don't believe it's the same. That issue propose some bigger changes to TS - while I'd only like to "reuse" the, just landed, logic for the situation described below.
✅ Viability 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, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
It's today possible to mimic dependent function parameters with tuples, spreads & co. Using such function parameters is clunky though because one can't destructure the arguments tuple without losing nice control flow analysis.
Since control flow analysis for destructured properties from unions has just landed on the main branch ( #46266 ), I think it would be great to extend the logic to cover this situation.
I've looked briefly at how this works for the newly introduced control flow analysis and I think the problem is that the information about the identifier coming from the arguments list is not propagated to getCandidateDiscriminantPropertyAccess
. So it has no chance of handling this "correctly". If we could pass this info there and then check the signature type, if it's spread+tuple based then we could apply the same logic as for binding patterns.
📃 Motivating Example
I've added a test case for this here: Andarist@3e87dec
type Args = ['A', number] | ['B', string]
declare function fn10(cb: (...args: Args) => void): void
fn10((kind, data) => {
if (kind === 'A') {
data.toFixed();
}
if (kind === 'B') {
data.toUpperCase();
}
})
Currently, with this technique, one could do this to keep the control flow analysis working:
type Args = ['A', number] | ['B', string]
declare function fn10(cb: (...args: Args) => void): void
fn10((...args) => {
if (args[0] === 'A') {
args[1].toFixed();
}
if (args[0] === 'B') {
args[1].toUpperCase();
}
})
but this ain't super friendly to the user.
💻 Use Cases
In XState (a state machine library) we have actions (can be thought of as functions/callbacks) that can be declared like this:
assign((context, event) => {
// this particular action can return a partial update to the context value
return {}
})
Since XState is a state machine library some combinations of context & event types are impossible. With this feature in place, we could explore computing valid combinations and maybe (since the task ain't trivial) we could narrow down types of the context
parameter based on different event.type
s. So for instance, there is a chance that this would become possible:
type Context = { userId: string | null }
assign((context, event) => {
if (event.type === 'CHANGE_PASSWORD') {
// `context` could be narrowed down to `{ userId: string }` if we could figure out how to create compute proper parameter tuples
}
return {}
})
cc @ahejlsberg