Skip to content

Local variables not narrowed in lambda function bodies #15631

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
paarthenon opened this issue May 6, 2017 · 8 comments
Closed

Local variables not narrowed in lambda function bodies #15631

paarthenon opened this issue May 6, 2017 · 8 comments
Labels
Duplicate An existing issue was already created

Comments

@paarthenon
Copy link

paarthenon commented May 6, 2017

Hello all, love your work with the language. I suspect I may have found a bug:

Local variables that are a union type (such as type | undefined) at declaration aren't being narrowed in lambda functions. Parameters and local aliases of those variables are.

TypeScript Version: 2.2.2 / 2.3.2 / nightly (2.4.0-dev.20170506)

Code

// A *self-contained* demonstration of the problem follows...

// example type
interface Test {
	label:string
}

// Function operates without errors, 'parameter' is correctly
// narrowed to Test rather than Test|undefined
function testParameters(parameter:Test|undefined) {
	if (parameter) {
		console.log(parameter.label);
		Promise.resolve().then(() => parameter.label);
	}
}

// Local variable is not narrowed in the lambda/arrow
function testLocals(parameter:Test|undefined) {
	let local = parameter;
	if (local) {
		console.log(local.label);
		Promise.resolve().then(() => local.label);
		//^ Error "Object is possibly 'undefined'."
	}
}

// Useful but annoying workaround
function workaround1(parameter:Test|undefined) {
	let local = parameter;
	if (local) {
		let localLabel = local.label;
		console.log(local.label);
		Promise.resolve().then(() => localLabel);
	}
}

// Useful but annoying workaround #2
function workaround2(parameter:Test|undefined) {
	let local = parameter;
	if (local) {
		let localPrime = local;
		console.log(local.label);
		Promise.resolve().then(() => localPrime.label);
	}
}

Expected behavior:

I expect union types to be narrowed in lambda functions defined inside of a block with a type guard.

Actual behavior:

Variables that are narrowed in the block a lambda is created are not interpreted as their narrowed type inside the body of the lambda function. The above examples also apply to union types with truthy variables like number | string and their appropriate type guards and to functions written as function(){...}.

@hediet
Copy link
Member

hediet commented May 7, 2017

Hi! This is intended (#8541), as (in general) there is no way to see whether the callback is invoked immediately and thus in the scope of all the if conditions that can assumed to be true, or much later, when the if conditions don't have to hold anymore.

@hediet
Copy link
Member

hediet commented May 7, 2017

I forget to mention: It works if you use const instead of let for local, as any assumptions about const values stay true regardless of the scope.

@paarthenon
Copy link
Author

paarthenon commented May 7, 2017

Thanks @hediet that helps. So if I understand you correctly we could realistically have a situation where the variable is reassigned before the callback executes so the type guard isn't necessarily valid at the time the callback executes. That seems fine, but then why does the compiler narrow function parameters which are also mutable?

If I change my earlier example about parameters to something that does mutate the parameter like this:

function testParameters(parameter:Test|undefined) {
	if (parameter) {
		console.log(parameter.label);
		setTimeout(() => parameter.label, 200); //Error "Object is possibly 'undefined'."
	}
	setTimeout(() => {
		parameter = undefined;
	}, 100)
}

Then the error comes up, which is pretty cool. If I remove the assignment to parameter, there are no errors.

This seems like a decent situation. Could the kind of analysis that's being done here be applied to local variables as well?

@hediet
Copy link
Member

hediet commented May 7, 2017

You don't need to wrap the assignment to parameter in a callback - just writing to it anywhere is sufficient:

function testParameters(parameter:Test|undefined) {
	if (parameter) {
		console.log(parameter.label);
		setTimeout(() => parameter.label, 200); //Error "Object is possibly 'undefined'."
	}
	parameter = undefined;
}

Theoretically, there is no reason why this analysis wouldn't do for variables introduced with let. But why would you declare a local variable with let over const when you don't intend to reassign it?

@eamodio
Copy link

eamodio commented May 7, 2017

@hediet thanks for the explanations.

Is there any easy/convenient way to get typescript to stop complaining about this in certain cases? Something that would have 0 effect on the output code? I know I could introduce another const variable to capture, but that would alter the output just to silence what I know is not a valid error (in certain cases).

Also while casting the variable as the type without undefined works, but is also quite verbose.

@hediet
Copy link
Member

hediet commented May 7, 2017

No, not that I know of. Sadly, there are no type assertions in TypeScript. You always have to call a function for that or directly check for null/undefined.

@RyanCavanaugh
Copy link
Member

Is there any easy/convenient way to get typescript to stop complaining about this in certain cases? Something that would have 0 effect on the output code? I

The ! postfix operator (local!.label) is exactly what you want - it removes null and undefined from the type of its operand.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label May 8, 2017
@eamodio
Copy link

eamodio commented May 8, 2017

@RyanCavanaugh that is awesome! Thank you!

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants