Skip to content

Refining discriminated union of arrays with array.find on a property that is a union #60785

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
elusiveunit opened this issue Dec 17, 2024 · 2 comments
Labels
Duplicate An existing issue was already created

Comments

@elusiveunit
Copy link

🔎 Search Terms

"discriminated union", "refine union", "array find union property"

🕗 Version & Regression Information

Tried every TypeScript version from 5.2.2 in the playground with the same result.

I found some similar issues that eventually led me to #42384, but I wasn't sure if the root cause is the same?

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.8.0-dev.20241217#code/JYOwLgpgTgZghgYwgAgMoAs5QA7IN4BQAkAPQnIBKEAtgPYBuoA5sgOQAqUwcITANhFbJ00FGFrImEMMi4ATZLRjIwI5AFcQwWiGTU4AawgBnZMBkB3WlAMA6YmACe2CAC42qAI7qsg5AB82Tm5eAVYAbmIEaxBoY3cQdWoAI2hIgF8CUEhYRBQKWk0FQiInF3dWAGFgKAQwgLYAeXo4PgjiKDg5YHV45ESUtIJMghhNBDBtXSkwSpi4gApjTBc+jCxsAG0AXQaCop2ASnxiMmQAUQAPFwmIBQADdZwGoogYUDv75GT1GSZaGSPTDPQL7EAKQKvd6xOT3KI6YwyGCFcFPXAAXmQyzgq1s0LkCyWKwgx3RAD4scTbGUUOi6R5vL5WIdIqRyAAFKC0FxQJxsaJQWJQYxCOS0Ez9AHICCXYCIxS6GkeYG4UEouSsexs5A6znc6B81gCoUi5BiiUgKUyuUyHQqZwoVhgjW2ZALABMAGZPQBOQ4daTqQXIZFFNEAflsxriGSAA

💻 Code

interface Sharp {
	// Removing 'Triangle' here to get rid of the union makes it work.
	type: 'Square' | 'Triangle';
	corners: number;
}
interface Round {
	type: 'Circle' | 'Oval';
	radius: number;
}

function getCorners(shapes: Sharp[] | Round[]) {
	// Expected `Sharp | undefined` but got `Sharp | Round | undefined`
	const foundSharp = shapes.find((shape) => shape.type === 'Square');
	// Property 'corners' does not exist on type 'Sharp | Round'.
	//   Property 'corners' does not exist on type 'Round'. (2339)
	return foundSharp?.corners;
}

🙁 Actual behavior

foundSharp is Sharp | Round | undefined even though it can never be Round due to type.

🙂 Expected behavior

foundSharp should be Sharp | undefined since only sharp shapes can have the type 'Square'.

Additional information about the issue

Setting type in the Sharp interface to only 'Square' makes it work as expected.

@jcalz
Copy link
Contributor

jcalz commented Dec 17, 2024

This isn't a TS bug. Your callback isn't inferred as returning a type predicate as implemented in #57465, because it would need to be a one-sided type predicate, which TypeScript doesn't have. The check shape.type === 'Square' can narrow Round | Sharp to Sharp but since shape.type !== 'Square' does not narrow Round | Sharp to Round, no type predicate can be inferred. (See flakyIsString in the text of #57465) If TS had one-sided type predicates as requested in #15048, then the return type could be inferred as something like shape is Shape else false and it would work. When you're using find() you don't care about the false side of the type guard function, but TS does, and there's currently no way to tell it not to.

If you want, you can annotate your callback with a type predicate return type, and while shape is Sharp will work, it's a bit of a lie. Something which works and is true is:

function getCorners(shapes: Sharp[] | Round[]) {
	const foundSharp = shapes.find(
		(shape): shape is typeof shape & { type: "Square" } =>
			shape.type === 'Square');
	return foundSharp?.corners;
}

Playground link to code

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Dec 17, 2024
@elusiveunit
Copy link
Author

I see, thanks for the explanation! We'll make do with some helpers then and keep an eye on the one-sided predicate issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants