Open dartess opened 3 weeks ago
It depends on how TypeScript breaks down your union. The inference targets are not at the same level so TypeScript can't discriminate this by how the target is contained in the union today.
It works OK when T
is directly contained in a union like this:
payload: { message: T | (() => T) } & { handler: Handler<T> }
With your variant is actually always finds 2 inference candidates:
type Candidate1 = () => { options: { market: string; }; }
type Candidate2 = { options: { market: string; }; }
But then which candidate gets selected always depends on the order (candidates are reduced from left to right). So, at times, Candidate1
gets selected. Then it fails constraint check and the final inferred type fallbacks to the constraint itself. That's what you are observing. It just happens that... you never see it because it lucked out and your arguments are assignable to the signature instantiated with that constraint. You might find this comment interesting if you are curious about some of the current implementation details.
A smile solution to fix this could involve filtering candidates themselves by constraint applicability (right now only selected inferred type gets matched against it). I assume this wasn't tried for performance reasons but I could be wrong.
Fixing this earlier - when candidates are collected might be difficult.
It's possible to work around this on your side as follows (TS playground):
declare const fnSymbol: unique symbol;
interface Function {
[fnSymbol]?: true;
}
type NotFunction<T> = T & {
[fnSymbol]?: never;
};
type Message = { options?: Record<string, unknown> };
type Handler<T> = (message: T) => void;
declare function subscribe<T extends Message>(
payload:
| ({ message: NotFunction<T> } & { handler: Handler<T> })
| ({ message: () => T } & { handler: Handler<T> }),
): void;
subscribe({
message: () => ({ options: { market: "market" } }),
handler: ({ options }) => {
// ^? (parameter) options: { market: string; }
},
});
π Search Terms
union order type infer
π Version & Regression Information
This is the behavior in every version I tried, and I reviewed the FAQ for entries about
union
/order
β― Playground Link
https://www.typescriptlang.org/play/?ts=5.5.4#code/C4TwDgpgBAshDO8CGBzaBeKBvKB7MwAlrgHbwD8AXFAEoQDGuATgCYA88wThJKANFACuJANYlcAdxIA+KAF8A3ACgloSFAASSEiwA2EJmwAqszAAoAtgmRpqRgJRR0sgG65CLZUpYNdSJtAAZsL0RKRQ8IIARvD03FEQxlAQAB7AEDrwsNaoENJmSlBFUGBIILq4SCyUhcVFAD7YUFaIuXbyUABkTQAW2noG1Fo6+oYm8rV1jTgtNhDUZo7OUEYd3Th9I4Oa-aPGsnK19tRuHl6RMXGECWZYtbNtUItOsrd4BMRk1DP+IhDA1AA5BZfv9AR05PY+LVNgMmAscPgwmR5EtZHc6pjigB6bFQAB65FqcmhkOUQA
π» Code
π Actual behavior
options
type isRecord<string, unknown>
π Expected behavior
options
type should be inferred as{ market: string }
Additional information about the issue
if you reverse the order of values in the union:
then the type will be inferred correctly