-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Keyword to permit inferring a union for a type parameter #44312
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
Comments
See Distributive Conditional Types And this will make your examples work. declare function fn1<T extends unknown[]>(items: T): T[number]
declare function fn2<T extends (() => unknown)[]>(items: T): ReturnType<T[number]>
declare function fn3<T extends {prop: unknown}[]>(items: T): T[number]['prop']
let res1: number | string = fn1([42, "hi"])
let res2: number | string = fn2([() => 42, () => "hi"]) // OK
let res3: number | string = fn3([{ prop: 42 }, { prop: "hi" }]) // OK type Check<T> = (value: unknown) => value is T
type CheckedType<T extends Check<unknown>> = T extends Check<infer U> ? U : never;
function string(): Check<string> {
return (value): value is string => typeof value === "string"
}
function number(): Check<number> {
return (value): value is number => typeof value === "number"
}
function union<T extends Check<unknown>[]>(...members: T): Check<CheckedType<T[number]>> {
return (value): value is CheckedType<T[number]> => {
for (let member of members) {
if (member(value)) {
return true
}
}
return false
}
}
let check1 = union(string(), number())
let x = '???' as string | number | {};
if (check1(x)) {
x // OK! x is string | number
} |
Sure but you've changed the signature of |
Aside: For clarity, this isn't "widening" (that process is only about converting literal types to their corresponding primitives, and some others). I don't think we have a name for the particular process of deciding when it's OK / not OK to infer a union for a type argument in the presence of multiple candidates. But it's clear in context what you mean here. For declare function noWiden<nowiden T>(arr: T[]): void;
const arr1 = [Math.random() > 0.5 ? 1 : "two"];
const arr2 = [1, "two"];
noWiden(arr1);
noWiden(arr2);
noWiden([1, "two"]); There's a ton of subtlety here around the inference candidate collection process - while the examples in the OP look similar, from a type system perspective the inference algorithm sees very different things. I wouldn't defend it as unsurprising but AFAIK we haven't actually broken much about this in the past (counterexamples welcomed for my own learning). I think the intuition here is something like "don't infer a union unless one of the inference candidates is a union"... but literally one of the inference candidates is a union in these cases. There'd have to be some stronger theoretical principle to rely on to make this work in a way that was at least somewhat consistent in the presence of refactoring an argument to a local.
Some more real-world use cases would be useful to help consider. |
I didn't really have a use case for
I guess you could rename declare function oneOf<prefer-union T>(a: T, b: T): T Which operates like this: oneOf("hello", "world") // string
oneOf("hello", 42) // string | number There's actually a number of ways TypeScript already has the desired oneOf(["hello"], [42]) // string[] | number[]
oneOf({ a: "hello" }, [42]) // { a: string } | number[] Then there are places where TypeScript is producing a union but possibly not ideal for // what it is today (without prefer-union):
oneOf({ a: "hello" }, { b: 42 }) // { a: string, b?: undefined } | { a?: undefined, b: number }
// should probably be:
oneOf({ a: "hello" }, { b: 42 }) // { a: string } | { b: number } And finally places where TypeScript bails out instead of producing a union: // what it is today (without prefer-union):
oneOf(() => "hello", () => 42) // Type 'number' (42) is not assignable to type 'string'
// should probably be:
oneOf(() => "hello", () => 42) // (() => string) | (() => number) |
This is our use case for inferring a union type. I didn't manage to work around having to manually declare the union type in the generic when calling our export function isOneOf<T>(
value: unknown,
guards: Array<(value: unknown) => value is T>
): value is T {
return guards.some((guard) => guard(value));
}
let a!: string | number | boolean;
const isString = (v: unknown): v is string => typeof v === "string";
const isNumber = (v: unknown): v is number => typeof v === "number";
if (isOneOf(a, [isString])) {
a; // string
}
if (isOneOf<string | number>(a, [isString, isNumber])) {
a; // string | number
}
// Type 'number' is not assignable to type 'string'.ts(2322)
if (isOneOf(a, [isString, isNumber])) {} |
Question: Which place makes more sense for TypeScript internally? function oneOf<widen T>(...guards: Array<(value: unknown) => value is T): value is T
// ^^^^^ declaration
function oneOf<T>(...guards: Array<(value: unknown) => value is widen T): value is T
// ^^^^^ reference |
If the definition for
it does not produce an error for
If instead you really wanted to constraint As for this
and
intended to express the same semantics? (I think so). Here is another example of what seem to be semantically identical generics where one has trouble. |
Suggestion
π Search Terms
β Viability Checklist
My suggestion meets these guidelines:
β Suggestion
Right now TypeScript has some rules about when it decides to widen a generic type or not, which can produce confusing results for relatively similar function signatures:
Right now its entirely outside of your control what TypeScript will decide to do when a type parameter is not provided and what TypeScript does has changed a few times over the years.
It would help a lot if TypeScript gave you some more control over this behavior, to specify if you want your generics to widen or not.
π Motivating Example
An increasingly popular way to use TypeScript is to produce types from runtime values, there are already lots of TypeScript features dedicated to making it easier to infer precise types from values, and there are even libraries like zod and io-ts to do things like:
Libraries written in TypeScript are increasingly relying on TypeScript's value-to-type inference as part of their public API. So when TypeScript changes its rules about things like when to widen vs not widen, it can be much more dramatic of a breaking change that sometimes requires redesigning these libraries.
Right now there are a couple hacks you can do to trick TypeScript into having the desired widening behavior (by lying about the actual types). But there is no guarantee that this behavior will stick around between TypeScript versions.
π» Use Cases
[Playground]
The text was updated successfully, but these errors were encountered: