-
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
Recursive type references #33050
Recursive type references #33050
Conversation
To be clear, do you intend to resolve nested Promise types? Another, do you also intend to improve Array#{flat,flatMap}? |
OK, so I expanded enough on this locally to try the type HypertextNode = string | [string, { [key: string]: any }, ...HypertextNode[]];
const hypertextNode: HypertextNode =
["div", { id: "parent" },
["div", { id: "first-child" }, "I'm the first child"],
["div", { id: "second-child" }, "I'm the second child"]
]; example out (which is a motivator here), and the approach quickly hits its limits - namely, when we compare tuples, eventually we compare the array methods on the tuple, which have |
So is this an alternative to #33018? |
@weswigham Latest commit changes deferred type reference instantiation to use the same logic as anonymous types. This ensures that deferred type references continue to be deferred when instantiated, and it fixes the issues you were seeing with infinite instantiations. The motivating example type HypertextNode = string | [string, Record<string, unknown>, ...HypertextNode[]];
const hypertextNode: HypertextNode =
["div", {id: "parent"},
["div", {id: "first-child"}, "I'm the first child"],
["div", {id: "second-child"}, "I'm the second child"]
]; now works as expected. I still need to look at pulling in the other changes in your diff. The |
@weswigham Printback now fixed by cherry picking some of your changes. A bit more context on the latest commits... The instantiation logic for array and tuple type references is now similar to that of anonymous types and mapped types: We track which outer type parameters are in scope at the location of the reference and cache instantiations based on the type arguments of those outer type parameters. This means self-referential array and tuple types are no more expensive than self-referential object types. |
# Conflicts: # src/compiler/checker.ts
@typescript-bot perf test this |
Heya @ahejlsberg, I've started to run the perf test suite on this PR at b18c70f. You can monitor the build here. It should now contribute to this PR's status checks. Update: The results are in! |
@ahejlsberg Here they are:Comparison Report - master..33050
System
Hosts
Scenarios
|
Performance tests above show a worst case 5% check time increase and 10% memory consumption increase. That's a bit too expensive. With the latest commit we only defer type argument resolution for aliased array and tuple types. That should improve the numbers. |
@typescript-bot perf test this |
Heya @ahejlsberg, I've started to run the perf test suite on this PR at 8f3a917. You can monitor the build here. It should now contribute to this PR's status checks. Update: The results are in! |
@ahejlsberg Here they are:
Comparison Report - master..33050
System
Hosts
Scenarios
|
Rather than creating a(nother) location with an observable inline vs not inlined checking change (which we usually attempt to remove), would it really be so bad to only make the deferred type in cases where we find the type to be circular (ie, when we spot it on the circularity stack)? It's not the first place where we'd be doing something other than returning an |
Performance numbers now look great. Basically zero cost to cover the known scenarios!
That might be fine too, though we're already at zero cost for the feature. I like the current syntactic solution because it is easy to reason about when it kicks in. I wasn't aware that we do something other than return |
Yeah, I've wondered about that, too, but it has yet to come up. In any case, that's why I cautiously wrote the |
Doesn't seem to work: type UnknownProps = Record<string, UnknownValues>
type UnknownValues = unknown | Record<string, UnknownProps>
Maybe I misunderstood it. |
In the opening comment by Anders the definition of the new kinds of types that can be circularly referenced are defined as:
The type interface R<T> {
[keys: string]: T;
}
type UnknownProps = R<UnknownValues>;
type UnknownValues = unknown | R<UnknownProps>; It is still the case that TypeScript is not going to resolve arbitrary symbols because there needs to be some indication that the recursion in guarded by something. |
Interesting. Also here's another way to write it: type UnknownProps2 = {[K in string]: UnknownValues2};
type UnknownValues2 = unknown | Record<string, UnknownProps2>
type StartValues2 = {[K in string]: number | Array<number> | StartValues2};
const test2: UnknownProps = {a: 1, b: {c: 2}} I heard 4.1 will have better support for recursive types? |
This is still throwing a circularity error in 4.0.5. export type Unpromise<T> = T extends PromiseLike<infer U> ? Unpromise<U> : T; EDIT:. That code works in |
+ Recursive types definition are allowed microsoft/TypeScript#33050 + Export the JSON type for reuse to avoid error like semantic error TS2345: Argument of type '(data: StationWorkerData) => void' is not assignable to parameter of type '(data: JSONValue) => JSONValue' Signed-off-by: Jérôme Benoit <[email protected]>
I do wish recursive types were possible with inference - for example, why can't this be type-hinted without explicitly having to declare a |
@mindplay-dk yes I agree I don't see why it wouldn't be possible to infer that. Feel free to create an issue for this. |
As far as I know, this code can not be implemented recursively. It can be done for the return type, but that would break the inference of the parameters. // Only implemented as type
const guardAll = {} as <A, B, C, D, E, F, G, H, I, J, K>(
guard1: RefineTypeGuard<unknown, A>,
guard2?: RefineTypeGuard<A, B>,
guard3?: RefineTypeGuard<B, C>,
guard5?: RefineTypeGuard<C, D>,
guard6?: RefineTypeGuard<D, E>,
guard7?: RefineTypeGuard<E, F>,
guard8?: RefineTypeGuard<F, G>,
guard9?: RefineTypeGuard<G, H>,
guard10?: RefineTypeGuard<H, I>,
guard11?: RefineTypeGuard<I, J>,
guard12?: RefineTypeGuard<J, K>
) => (value: unknown) => value is A & B & C & D & E & F & G & H & I & J & K
type RefineTypeGuard<A, B> = (
value: A,
...args: readonly unknown[]
) => value is B extends A ? B : never;
// Create new predicate by combining them in order
const isFoobar = guardAll(
// Don't have to tell TypeScript what the type of val is, it is inferred from the last predicate
(val): val is string => typeof val === 'string',
(val): val is `foo${string}` => val.startsWith(val),
(val): val is 'foobar' => val === 'foobar'
);
const test = {} as unknown;
if (isFoobar(test)) {
test; // "foobar"
} |
@nicobrinkkemper Is this the sorta thing you're looking for? Playground Link |
Not exactly. See this example and hover over the functions in |
@nicobrinkkemper That's the purpose of |
What I'm talking about is the limitation to infer the parameters of the Type Guard. In your playground link (which is nice don't get me wrong) you are forced to type your parameters. In my playground I don't have to specify them, and if you hover over them you see the type of the previous predicate. This would mean that any invalid function will be catched right there on the spot and it would show a red line under that specific offending function. For example if I add a invalid function to the pipe in my playground example, it shows:
|
Ahh, I see now what you mean about the differences and why you might not be interested in that particular implementation. I have some similar code to that playground, and yeah the errors end up being moved to the |
With Record still causes error:
|
I'm using TypeScript 4.5 and still get Code: const func = (test: Test) => ({ ...test });
interface Test {
test: ReturnType<typeof func>;
} Minimum reproducible code sandbox: Related issue: |
This PR implements support for recursive type references. For example:
Previously, the above would have multiple circularity errors.
The specific change made by this PR is that type arguments are permitted to make circular references in aliased types of the following kinds:
Array<Foo>
).Foo[]
).[string, Foo?]
).A type
T
is said to be an aliased type whenT
is the right hand side of a type alias declaration, orT
is a constituent of an aliased union, intersection, indexed access, or conditional type, orT
is an aliased parenthesized type.For example, in the type alias declaration
the type argument
ValueOrArray<T>
occurs in an aliased type instantiation that is a constituent of an aliased union type that is the right hand side of a type alias declaration, and the type argument is therefore permitted to be a circular reference.An additional change in this PR is that when an array type, a tuple type, or an instantiation of a generic class or interface type is the right hand side of a type alias declaration, that type alias becomes the name by which the type is referenced in compiler diagnostics and quick info. Previously, such aliased types were always shown in their expanded form.
NOTE: For consumers of the TypeScript compiler APIs this PR is potentially a breaking change because it removes the
typeArguments
property from theTypeReference
interface and provides agetTypeArguments
method on theTypeChecker
interface to be used instead. This change is necessary because resolution of type arguments in type references is now deferred.Fixes #3496.
Fixes #6230.
Fixes #14174.
Fixes #28929.
Fixes #32967.