Repository navigation
Type narrowing of tagged union this lost inside arrow function in class methods #29260
Description
Activity
In the general case, it's possible that "this" will change its structure before a callback is executed
AnyhowStep is that not also true in the case where we assign
thisto a new variable,self, and use that variable in the closure?It's generally because your callback may not be immediately invoked, so
thismay be mutated before your callback is executed. Aconstreassignment is used as a hint that it's unlikely to change.- addedQuestionAn issue which isn't directly actionable in codeAn issue which isn't directly actionable in code
on Jan 4, 2019 I'm trying to follow this reasoning, but it seems to me that
thiscan not be mutated in a way that would makethis.pinvalid inside the arrow function without jumping through significant hoops to work around the type system.A simple
delete this.pwould make the assumption ofpexisting invalid.Wesley Wigham (@weswigham), I have to agree with Caleb (@calebharris) on this; that reasoning does not make sense to me. By that logic, type narrowing should be invalidated after function calls,
await, any use offor...ofor the spread operator (which might invoke a side-effecting iterator implementation), any operation that might trigger a side-effectingtoString(), or any property access (which might invoke a side-effecting getter orProxy). In any of those situations, some code may reach in and unexpectedly mutatethisin a way which would invalidate the type narrowing.The case of
awaitreally makes it clear how inconsistent this behavior is:type DivElementlike = Elementlike & { tag: 'div' }; type SpanElementlike = Elementlike & { tag: 'span' }; function useDiv(div: 'div') { } async function doSomethingAsync() { } class Elementlike { public tag: string; private _content: string; hasTag(t: 'div'): this is DivElementlike; hasTag(t: 'span'): this is SpanElementlike; hasTag(t: string): boolean { return this.tag === t; } async doSomething(): Promise<void> { if (this.hasTag('div')) { // This useDiv() call type checks. useDiv(this.tag); // This useDiv() call also type checks, even though it will run at some // arbitrary future time. await doSomethingAsync(); useDiv(this.tag); // This useDiv() call does not type check, because the type of 'this' // is not narrowed inside the arrow function. However, this behaves // the same as the 'await' call above! return doSomethingAsync().then(() => { useDiv(this.tag); }); } } }
(Here's that code on the playground.)
It seems to me that changing this behavior would be a win. It aligns better with user expectations and makes type predicates more useful, and keeping this limitation doesn't really improve safety significantly.
You can read #9998 for details on why we bound our analysis where we do today.
I read through it. Those tradeoffs do make a great deal of sense to me, and I can understand that you have to draw them somewhere.
I don't want to construct a strawman if it's not what you intended, but a major topic in that discussion is the impracticality of interprocedural control flow analysis. I do want to point out that the issue we're discussing here doesn't require any sort of interprocedural analysis or inlining.
What I would like to see, ideally, is this. (Pretend for a moment that
thisparameters are allowed on arrow functions.) Currently the arrow function insidedoSomething()is treated as if it had this type:(this: Elementlike) => any
However, I'd like its type to reflect the narrowing of
thisby the type predicate:(this: Elementlike & { tag: 'div' }) => any
Nothing meaningfully interprocedural is happening there; this is just a change in how values within a single function are typed.
Again, hopefully I'm not just setting up a strawman, but that seemed worth pointing out.
I've updated the sample code with the following snippet:
function bat(baz: Foo | Bar) { if (baz.tag === "bar") return; // it also works outside of class methods const x = run(() => baz.p); }
This compiles just fine, even though by my reading, it's the same situation. I read through #9998 and have a better understanding now of the trade-offs we're dealing with here. However, I feel like the behavior could/should be more consistent.
This issue seems to frequently affect me when writing React code that utilizes nested
render()methods, ie:type State = { status: 'LOADING' } | { status: 'READY'; data1: string; data2: number }; class MyComponent extends React.Component<any, State> { public render() { return (<div> {this.state.status === 'READY' && ( <Formik initialValues={this.state.data1} /* <-- This is OK */ render={(props) => (<OtherComponent data={this.state.data2} {...props} />)} /* <-- this.state loses narrowing here */ /> )} </div>); } }
In the above example, even though I've narrowed the tagged union, TypeScript won't let me use it in the inner
render()method (even though TS did correctly narrow the type ofthis.statewhen it's used as a component property).TypeScripts behavior indeed seems baffling at first, as
this.stateis actuallyReadonly<State>inside of a React Component class, meaning that the properties onthis.stateshouldn't ever change.However, (thanks to Wesley Wigham (@weswigham)'s helpful link above!) I now understand that the TypeScript compiler is actually worried that the entirety of
thisorthis.statemight be swapped out between the time that my outerrender()method is called, and the time that Formik's innerrender()is called.In this specific instance, this wouldn't be a problem in practice, but it's impossible for Typescript to know that Formik's inner
render()is called immediately, and has no side-effects.
The solution to this problem ends up being rather easy. We just need to get a hold of a value of
statethat we know won't change:type State = { status: 'LOADING' } | { status: 'READY'; data1: string; data2: number }; class MyComponent extends React.Component<any, State> { public render() { const state = this.state; return (<div> {state.status === 'READY' && ( <Formik initialValues={state.data1} render={(props) => (<OtherComponent data={state.data2} {...props} />)} /> )} </div>); } }
I've posted a variant of the above example to CodeSandbox if anybody wants to play around with it.
Reacted by Damian Rivastypescript-bot commented
on Jul 13, 2019 ContributorMore actionsThis issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.
- locked as resolved and limited conversation to collaborators
on Oct 21, 2025
TypeScript Version: 3.2.2, 3.3.0-dev.20190104
Search Terms:
this type discriminated union narrowing
Code
Expected behavior:
Code compiles correctly
Actual behavior:
Compilation fails with the following message:
Playground Link:
https://www.typescriptlang.org/play/index.html#src=class%20Baz%20%7B%0D%0A%20%20test(this%3A%20Foo%20%7C%20Bar)%20%7B%0D%0A%20%20%20%20if%20(this.tag%20%3D%3D%3D%20%22bar%22)%20return%20false%3B%0D%0A%0D%0A%20%20%20%20%2F%2F%20this.p%20is%20correct%20here...%0D%0A%20%20%20%20console.log(this.p)%3B%0D%0A%0D%0A%20%20%20%20%2F%2F%20but%20not%20here...%0D%0A%20%20%20%20const%20x%20%3D%20run(()%20%3D%3E%20this.p)%3B%0D%0A%0D%0A%20%20%20%20%2F%2F%20yet%20it%20works%20if%20we%20assign%20%60this%60%20to%20something%20else%0D%0A%20%20%20%20const%20self%20%3D%20this%3B%0D%0A%20%20%20%20const%20y%20%3D%20run(()%20%3D%3E%20self.p)%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0Aclass%20Foo%20extends%20Baz%20%7B%0D%0A%20%20tag%3A%20%22foo%22%20%3D%20%22foo%22%3B%0D%0A%20%20p%3A%20boolean%20%3D%20true%3B%0D%0A%7D%0D%0A%0D%0Aclass%20Bar%20extends%20Baz%20%7B%0D%0A%20%20tag%3A%20%22bar%22%20%3D%20%22bar%22%3B%0D%0A%7D%0D%0A%0D%0Afunction%20run(f%3A%20()%20%3D%3E%20boolean)%20%7B%0D%0A%20%20return%20f()%3B%0D%0A%7D%0D%0A%0D%0Afunction%20id(b%3A%20boolean)%20%7B%0D%0A%20%20return%20b%3B%0D%0A%7D%0D%0A
Related Issues:
Issue #28763 might be related, but I'm not sure.