Skip to content

With exactOptionalPropertyTypes enabled, assigning a union with missing literal properties is incorrectly allowed #61678

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

Open
loganzartman opened this issue May 9, 2025 · 1 comment · May be fixed by #61682
Assignees
Labels
Bug A bug in TypeScript

Comments

@loganzartman
Copy link

loganzartman commented May 9, 2025

🔎 Search Terms

"exactoptionalpropertytypes union", "function call union assignability", "discriminated union assignment allowed incorrectly", "discriminated union assignable missing property"

🕗 Version & Regression Information

  • This changed between versions 4.3.5 and 4.4.4
  • still exists in nightly 5.9.0

⏯ Playground Link

https://www.typescriptlang.org/play/?exactOptionalPropertyTypes=true&ts=5.9.0-dev.20250508#code/KYDwDg9gTgLgBDAnmYcCqcC8cCQBYAKBwB84BvJFALjgHIBBWgbjgDcBDAGwFdgaA7bp04BfQiXKU+dAELM2XXjQDOMKAEt+AcxFNChAGbd+AYxjqI-OCa6cAPABUAfAAoDNFwEosTuA880DuRQwDDcUFYGXmIEhKCQsHBGpuaWScZmFvwAwrZeNBhk4gD0xXDKABYQQgAmcMBQUNBwAEbc8DUQwMpw-BAw4iFhEdZ5Xj7k4jjqBnAuALLsMBUAdFDs-J0Atl7eRUQ4OKW9-XDsysrqWvzsLZyoMBDoc1vqF5pacAAGHDzSgsIvp4pjghuErBRkNIGLRdFMYlMwSNIdRZLQADQKP40WgwbowWF6IgiTxEhEEeLQeDJTJpGrqEJmABKoXB+We+2mswWS1W602EB2nj2U2ODihdDICChNAARPRZSwRLQ4G8TvBzpdrrd7ggnlI6GhaCtCHBcEcymaAApNFCwRB0X68FVq17vbSqqwG2hSqRyhVKlVteAhACO3AZwDqmmlKElsek8sVWKUvSEnEDJoOSIhfrojDhxP0RBzkhlaMxTuheNUhMIMSAA

💻 Code

export type U = 	
	| {type: 'A'; value: null}
	| {type: 'B'; value: string};

function call<T>(f: () => T): T {return f()}

export function functionCall(): U {
	// should error but does not
	return call(() => {
		if (Math.random()) {
			// not assignable to U (missing `value: null`)
			return {type: 'A'};
		}

		return {type: 'B', value: 'test'};
	});
}

export function directReturn(): U {
	if (Math.random()) {
		// Type '{ type: "A"; }' is not assignable to type 'U'.
  		//   Property 'value' is missing in type '{ type: "A"; }' but required in type '{ type: "A"; value: null; }'.
		return {type: 'A'};
	}

	return {type: 'B', value: 'test'};
}

🙁 Actual behavior

type error in directReturn, but no type error in functionCall

🙂 Expected behavior

type error in both directReturn and functionCall, because {type: 'A'} is not assignable to {type: 'A', value: null}

Additional information about the issue

It does work with exactOptionalPropertyTypes disabled: Playground

With exactOptionalPropertyTypes enabled, the return type of call() is inferred as:

{
    type: "A";
    value?: never;
} | {
    type: "B";
    value: string;
}

whereas with it disabled, it's inferred as:

{
    type: "A";
    value?: undefined;
} | {
    type: "B";
    value: string;
}

But this doesn't fully explain it, because even assigning the result of call() to a variable before returning it causes the expected error:

export function functionCall(): U {
	const result = call(() => {
		if (Math.random()) {
			// not assignable to U (missing `value: null`)
			return {type: 'A'};
		}

		return {type: 'B', value: 'test'};
	});
	// error as expected
	return result;
}
@LukeAbby
Copy link

LukeAbby commented May 9, 2025

value doesn't need to be falsey. You can do { type: "A"; value: 123 | 456 } | { type: "B"; value: string } as well. it appears to be any literals in fact.

You can also get rid of some of the extra fluff. All in all you can reduce it to this:

type SomeLiteral = ...;
export type U = { type: "A"; value: SomeLiteral } | { type: "B"; value: string };

// Error
const directAssignment: U = Math.random() ? { type: "A" as const } : { type: "B" as const, value: "test" }

const indirect = Math.random() ? { type: "A" as const } : { type: "B" as const, value: "test" }
const indirectAssignment: U = indirect; // Ok

If I had to guess there's some specialized check that missed this edge case. It seems quite precisely targeted when the payload is also discriminated. For example:

type Check1 = { type: "A"; value?: never } extends { type: "A"; value: 123 } | { type: "B"; value: string } ? true : false
//   ^ true

type Check2 = { type: "A"; value?: never } extends { type: "A"; value: 123 } ? true : false
//   ^ false

type Check3 = { type: "A"; value?: never } extends { type: "A"; value: 123 } | { type: "B" } ? true : false
//   ^ false

type Check4 = { type: "A"; value?: never } extends { type: "A"; value: 123 } | { type: "B"; value: never } ? true : false
//   ^ true

Perhaps there's some incorrect "fusion" where { type: "A"; value: 123 } | { type: "B"; value: string } is being treated like { type: "A" | "B"; value: never | 123 } = { type: "A" | "B"; value: 123 }?

@loganzartman loganzartman changed the title With exactOptionalPropertyTypes enabled, returning an object with missing falsy properties is incorrectly allowed With exactOptionalPropertyTypes enabled, assigning an object with missing properties is incorrectly allowed May 9, 2025
@loganzartman loganzartman changed the title With exactOptionalPropertyTypes enabled, assigning an object with missing properties is incorrectly allowed With exactOptionalPropertyTypes enabled, assigning a union with missing literal properties is incorrectly allowed May 9, 2025
@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label May 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
4 participants