-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Allowing infer
after extends T
in conditional types would simplify complicated type definitions massively
#47330
Comments
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 |
This 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. |
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.
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), 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 I've confirmed that removing at least one conditional type in I thought the following patch could be a workaround, but it didn't help. Unwanted literals such as @@ -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 @RyanCavanaugh Is this straightforward and motivating? Please let me know if there is any room for further elaboration. |
@yudai-nkt I think this a misunderstanding - almost certainly, we wouldn't change the distributivity behavior under this construct |
@RyanCavanaugh I don't want the distributivity behavior to be changed. Let me rephrase my issue and possible solutions. My original 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. In both cases, the nest depth is reduced to 3 and I still have a type parameter |
Sure, but nothing is going to make type M = { next: M | { data: number } };
type F = DottedKey<M>; |
Yes, but |
q: is there any info anywhere on what 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 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 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 |
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 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; |
@RyanCavanaugh I think this is basically solved by the mentioned #48112 - the only difference is in the allowed order between 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 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}` }> |
It seems to work if we rewrite to an alternate 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" |
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 |
#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 |
I 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
infer
stumped me a whole lot when I was learning).Before:
After:
Alternatives
as
keyword 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
The text was updated successfully, but these errors were encountered: