Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upRecursive 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 |
I believe 5% performance is very acceptable trade for this feature (is it outside of the margin of error at all?). The recursive type references should just be enabled for all cases. |
@RWalkling hey, sorry to be a bother, but I have a question in regards to your unwarp construct. I have successfully converted a tree-like structure into a recursive nesting of type Recursive<T> =
| T
| Plain<Recursive<T>>
export type Flatten<T> = T extends Recursive<infer R> ? R : never; did not work for me, instead producing the same type I passed into the |
@icehaunter Try recursively extracting in export type Flatten<T> = T extends Plain<infer U> ? Flatten<U> : T; |
@isiahmeadows that doesn't work, because of error "Type alias 'Flatten' circularly references itself". Because the recursive type usage is not inside an interface. |
@icehaunter type Flatten<T> = {
0: T,
1: T extends Plain<infer U> ? Flatten<U> : never,
}[T extends Plain<any> ? 1 : 0] If it weren't so difficult and time-consuming (as basically every type system more complicated than C or Hindley-Milner is), I'd reimplement TS myself using proper deductive types with |
Now requires TypeScript 3.7 Record isn't used due to microsoft/TypeScript#33050 (comment)
I still get type TreeNode = {
// ^--------^ Type alias 'TreeNode' circularly references itself
[key: string]: TreeNode | [string, string],
} & {path: string}; |
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 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.