Repository navigation
Allowing infer after extends T in conditional types would simplify complicated type definitions massively #47330
Description
Activity
I think what you're looking for already exists, with a slightly different syntax:
type Is<T extends U, U> = T
Translating your second version directly:
type Is<T extends U, U> = T; type InferEventFromTypeMatch<TypeMatchString extends string> = TypeMatchString extends Is<infer ExactTypeMatchString, Event['type']> // Can give a much more meaningful label ? Extract<Event, AbstractEvent<ExactTypeMatchString, any>> : TypeMatchString extends `${infer WildcardedTypeMatch}*` ? Event extends Is<infer WildcardTypeMatch, { type: `${WildcardedTypeMatch}${any}` }> // Can give a much more meaningful label here too ? WildcardTypeMatch : never : never // Only two hanging nevers, rather than four
But in your specific example you don't need it at all:
type InferEventFromTypeMatch<TypeMatchString extends string> = TypeMatchString extends Event['type'] // Can give a much more meaningful label ? Extract<Event, AbstractEvent<TypeMatchString, any>> : TypeMatchString extends `${infer WildcardedTypeMatch}*` ? Event extends { type: `${WildcardedTypeMatch}${any}` } // Can give a much more meaningful label here too ? Event : never : never // Only two hanging nevers, rather than four
There's a cost to inference, so in idiomatic code we do not use it to rename types. Narrowing a type with
T extends Somethingand then usingTis not hard to follow, once you get used to it.Reacted by Toni Villena and Josef WittmannRyanCavanaugh commented
on Jan 12, 2022 MemberMore actionsThis is already how it works?
type Testable = { test(): void }; type GetTest<T extends Testable> = { m: T }; // Nothing needed here to match the constraint type Foo<T> = T extends Testable ? GetTest<T> : never;
A more straightforward motivating example would be helpful here.
- addedNeeds More InfoThe issue still hasn't been fully clarifiedThe issue still hasn't been fully clarified
on Jan 12, 2022 tl;dr I need this feature as well but for a different reason. This feature can help us avoid reaching the recursion depth limit.
I came across this issue while searching for a duplicate of what I was going to submit.
Narrowing a type with
T extends Somethingand then usingTis not hard to follow, once you get used to it.I agree with this. Renaming type parameters won't affect readability much once you learn the syntax of conditional type. That said, I still find this feature useful to mitigate the compiler's recursion depth limit. Let me illustrate the problem I'm having right now.
In the example below (playground),
DottedKeyreceives one object type parameter and resolves to a union of all the possible property access chained with a dot.type DottedKey<T, K extends keyof T = keyof T> = K extends string ? T[K] extends Record<string, any> ? T[K] extends infer V ? V extends (infer E)[] ? K | `${K}.${number | "length"}` | `${K}.${number}.${DottedKey<E>}` : K | `${K}.${DottedKey<V>}` : never : K : never; // "foo" | "foo.bar" type T0 = DottedKey<{foo: {bar: string}}> // "foo" | `foo.${number}` | "foo.length" | `foo.${number}.bar` type T1 = DottedKey<{foo: {bar: string}[]}> // "foo" | "foo.bar" | "foo.baz" type T2 = DottedKey<{foo: {bar: string} | {baz: number}}>
While this works as expected with simple types, giving complex types yields a compile error
TS2589: Type instantiation is excessively deep and possibly infinite.. I want to useDottedKeywith the constituents of@types/estree'sNode, and they are complex enough to cause the error.I've confirmed that removing at least one conditional type in
DottedKeyfixes the error in my use case, but they are all required including the seemingly redundantT[K] extends infer V, which enables union distribution. So I can't go that way.I thought the following patch could be a workaround, but it didn't help. Unwanted literals such as
foo.bar.charAtbecome assignable toT0. This comes from the fact that[T[K], T[K]] extends [Record<string, any>, infer V]somehow evaluates to true whenTis{bar: string}(reproduction). I haven't figured out why yet.@@ -1,9 +1,7 @@ type DottedKey<T, K extends keyof T = keyof T> = K extends string - ? T[K] extends Record<string, any> - ? T[K] extends infer V + ? [T[K], T[K]] extends [Record<string, any>, infer V] ? V extends (infer E)[] ? K | `${K}.${number | "length"}` | `${K}.${number}.${DottedKey<E>}` : K | `${K}.${DottedKey<V>}` - : never : K
That's why I need this feature. If something like
T[K] extends Records<string, any> infer V(open to better syntax ideas) is available, recursion depth won't hit its limit without breaking the definition ofDottedKey.Ryan Cavanaugh (@RyanCavanaugh) Is this straightforward and motivating? Please let me know if there is any room for further elaboration.
RyanCavanaugh commented
on Feb 3, 2022 MemberMore actionsYudai Nakata (@yudai-nkt) I think this a misunderstanding - almost certainly, we wouldn't change the distributivity behavior under this construct
Ryan Cavanaugh (@RyanCavanaugh) I don't want the distributivity behavior to be changed. Let me rephrase my issue and possible solutions.
My original
DottedKeyimplementation has 4 nested conditional types and I want to reduce the number to 3 or less. My workaround in the previous comment would have reduced it to 3, but it is not working unfortunately.type DottedKey<T, K extends keyof T = keyof T> = K extends string ? [T[K], T[K]] extends [Record<string, any>, infer V] ? V extends (infer E)[] ? K | `${K}.${number | "length"}` | `${K}.${number}.${DottedKey<E>}` : K | `${K}.${DottedKey<V>}` : K : never;
The feature requested by the issue author will also reduce the number of conditional types.
[T[K], T[K]] extends [Record<string, any>, infer V]will be replaced with something likeT[K] extends Record<string, any> infer V.In both cases, the nest depth is reduced to 3 and I still have a type parameter
V, which can be distributive in a conditional type (unlikeT[K]).RyanCavanaugh commented
on Feb 3, 2022 MemberMore actionsSure, but nothing is going to make
DottedKeynot fail on at least some types.type M = { next: M | { data: number } }; type F = DottedKey<M>;
Yes, but
Mdoesn't need to be covered byDottedKeyfor my specific use case. So I don't really care at the moment.Reacted by Andrii DieievAndarist commented
on Feb 25, 2022 ContributorMore actionsq: is there any info anywhere on what
T extends infer R ? R : neverdoes and how it affects other parts of the inference algorithm? Intuitively I've assumed that it's forcing the compiler to resolve T based on the information it was able to gather.I've assumed that in a situation like this it wouldn't have any effect on how typechecking works for the argument that should be typechecked with the outcome of
ComputeStuff:interface TypegenDisabled { "@@xstate/typegen": false; } interface TypegenEnabled { "@@xstate/typegen": true; } type TypegenConstraint = TypegenEnabled | TypegenDisabled; type ComputeStuff<TTypesMeta> = TTypesMeta & { /* do stuff */ }; declare function createMachine< TTypesMeta extends TypegenConstraint = TypegenDisabled >( config: { tsTypes?: TTypesMeta }, implementations: TTypesMeta extends infer EvaluatedTypesMeta ? ComputeStuff<EvaluatedTypesMeta> : never ): void;
Unfortunately, it changes the behavior "down the road" but since
tsTypesis an optional property I also need to "resolve" it quickly toTypegenDisabledcase, and while this trick works for that, some other test cases of mine break due to the mentioned change of behavior.This is just an example to illustrate what I'm doing but I'm really after "fixing" it - I would like to better understand how I could influence the order in which TS infers stuff. Basically, I know where the information I need is in the input arguments but I'm not sure how to best control what should be "deferred" by TS. Note that I'm aware of
NoInfer,LowInfer, and other stuff but this is still just a magic box that I find hard to manage in a complex scenario.I'm trying 4.7 beta and it looks like #48112 fixes this issue although recursion limit is not relaxed yet.
I'm trying to understand the motivation behind this issue and #48112.
Considering the first post of this issue, what's the difference between
U extends SomeConcreteType ? U : neverandU & SomeConcreteType?Before:
type InferringType<T> = T extends infer U ? U extends SomeConcreteType ? U : never : never
After:
type InferringType<T> = T extends infer U ? (U & SomeConcreteType) : never
And in the Motivating Example, isn't it equivalent to this?
type InferEventFromTypeMatch<TypeMatch extends string> = TypeMatch extends Event['type'] ? Extract<Event, AbstractEvent<TypeMatch, any>> : TypeMatch extends `${infer WildcardedTypeMatch}*` ? Event & { type: `${WildcardedTypeMatch}${any}` } : never
The announcement for #48112 has an example of 3 equivalent declarations, the last one of which is added by the pull request:
type FirstIfString<T> = T extends [infer S, ...unknown[]] ? S extends string ? S : never : never;
type FirstIfString<T> = T extends [string, ...unknown[]] // Grab the first type out of `T` ? T[0] : never;
type FirstIfString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
Isn't that also equivalent to this?
type FirstIfString<T> = T extends [infer S, ...unknown[]] ? (S & string) : never;
I realize the pull request is useful for cases like this:
type StringBox<T extends string> = {x:T}; type FirstIfString<T> = T extends [infer S, ...unknown[]] ? StringBox<S & string> : never; type FirstIfString2<T> = T extends [infer S extends string, ...unknown[]] ? StringBox<S> : never; type FirstIfString2Alt<T> = T extends [infer S, ...unknown[]] ? S extends string ? StringBox<S> : never : never; type D = FirstIfString<[boolean, number, string]>; // { x: never } type D2 = FirstIfString2<[boolean, number, string]>; // never type D3 = FirstIfString2Alt<[boolean, number, string]>; // never
They really should have put a better example in the announcement.
Update: actually, isn't this even simpler?
type StringBox<T extends string> = {x:T}; type FirstIfString<T> = T extends any[] ? T[0] extends string ? StringBox<T[0]> // T[0] in original example : never : never;
Andarist commented
on Mar 10, 2023 ContributorMore actionsRyan Cavanaugh (@RyanCavanaugh) I think this is basically solved by the mentioned #48112 - the only difference is in the allowed order between
inferand extraextends. The OP's example can be rewritten (using the added capability) as follows:type InferEventFromTypeMatch<TypeMatchString extends string> = TypeMatchString extends infer ExactTypeMatchString extends Event["type"] ? Extract<Event, AbstractEvent<ExactTypeMatchString, any>> : TypeMatchString extends `${infer WildcardedTypeMatch}/*` ? Extract<Event, AbstractEvent<`${WildcardedTypeMatch}/${any}`, any>> : never;
type InferEventFromTypeMatch<TypeMatchString extends string> = TypeMatchString extends infer ExactTypeMatchString extends Event["type"] ? Extract<Event, AbstractEvent<ExactTypeMatchString, any>> : TypeMatchString extends `${infer WildcardedTypeMatch}/*` ? Extract<Event, AbstractEvent<`${WildcardedTypeMatch}/${any}`, any>> : never;
How is this different from my example?
type InferEventFromTypeMatch<TypeMatch extends string> = TypeMatch extends Event['type'] ? Extract<Event, AbstractEvent<TypeMatch, any>> : TypeMatch extends `${infer WildcardedTypeMatch}*` ? Event & { type: `${WildcardedTypeMatch}${any}` } : never
Also, OP's example doesn't have the extra
/characters in the strings.
I tried rewriting OP's example, and I ended up with
type InferEventFromTypeMatch<TypeMatch extends string> = TypeMatch extends Event['type'] ? Extract<Event, AbstractEvent<TypeMatch, any>> : TypeMatch extends `${infer WildcardedTypeMatch}*` ? Extract<Event, { type: `${WildcardedTypeMatch}${any}` }> : never
This is the same as my past version, except
Event & { type: `${WildcardedTypeMatch}${any}` }
and
Extract<Event, { type: `${WildcardedTypeMatch}${any}` }>
Sure, but nothing is going to make
DottedKeynot fail on at least some types.type M = { next: M | { data: number } }; type F = DottedKey<M>;
It seems to work if we rewrite to an alternate
DottedKeyA: (playground)type DottedKey<T, K extends keyof T = keyof T> = K extends string ? T[K] extends Record<string, any> ? T[K] extends infer V ? V extends (infer E)[] ? K | `${K}.${number | "length"}` | `${K}.${number}.${DottedKey<E>}` : K | `${K}.${DottedKey<V>}` : never : K : never; type DottedKeyA<T, K extends keyof T = keyof T> = K extends string ? T[K] extends Record<string, any> ? T[K] extends (infer E)[] ? K | `${K}.${number | "length"}` | `${K}.${number}.${DottedKeyA<E>}` : K | `${K}.${DottedKeyA<T[K]>}` : K : never; type M = { next: M | { data: number } }; type F = DottedKey<M>; // Type instantiation is excessively deep and possibly infinite.(2589) type FA = DottedKeyA<M>; // "next"
Andarist commented
on Mar 11, 2023 ContributorMore actionsHow is this different from my example?
Sorry, I didn't notice your post. Either way, there are different ways to write this type - the point is (also mentioned by you) that
infer+extendscombination (almost like proposed by the OP) is already available.#48112 resolves the issue in the original post, but perhaps the recursion limit issue mentioned in the comments should be resolved (perhaps this issue should be closed as complete, and the recursion limit should be opened as a new issue?).
As OP's motivating example can be rewritten without the new feature
infer A extends B, which I mentioned in my comments before I realized that @webstrand's earlier comment had already mentioned it, I don't feel it is a good motivating example. There are better examples in this comment at #48112.Andarist commented
on Mar 14, 2023 ContributorMore actionsI think that the recursion limit issue is mostly mitigated by the ability to write tail recursive conditional types:
type JoinPath<A extends string, B extends string> = [A] extends [never] ? B : [B] extends [never] ? A : `${A}.${B}`; type _DottedKey< T, Path extends string = never, R extends string = never > = T extends readonly (infer E)[] ? _DottedKey< E, JoinPath<Path, `${number}`>, R | JoinPath<Path, `${number | "length"}`> > : T extends Record<string, unknown> ? keyof T extends infer K extends string ? _DottedKey<T[K], JoinPath<Path, K>, R | JoinPath<Path, K>> : never : R; type DottedKey<T> = _DottedKey<T>;
Suggestion
🔍 Search Terms
✅ Viability Checklist
My suggestion meets these guidelines:
⭐ Suggestion
Doing this would allow the complete removal of an entire branch, and it's much more intuitive (the hanging
inferstumped me a whole lot when I was learning).Before:
After:
Alternatives
askeyword in template string literal types e.g.📃 Motivating Example
This playground link - has some documentation that should provide some context for the following code, but this is a pretty universal pattern anyway.
Before:
After:
💻 Use Cases