-
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
Type alias circularly references itself #14174
Comments
See the explanation here for more info. |
type Foo = Bar<Foo> Illegal because we don't know that the definition of
|
Don't you have access to the definition of |
and now we're going infinitely deep. Type aliases and interfaces are subtly different and there are rules about self-recursion for aliases that don't apply to interfaces. Because an alias is supposed to always be "immediately expandable" (it's as if it were an in-place expansion of its referand), there are things you can do interfaces you can't do with type aliases. |
Cycles can be detected and you can give up at that point, but as long as you can topologically order the type aliases by their references to one another it's fine. Haskell, PureScript, Scala etc have no problem with this. |
I mean, we could, it's just that writing |
Sure, you can do that to emulate type Foo = Bar<Foo> But what about type Foo = Bar<Foo> | Baz<Foo> In this situation afaict the only solution is to expand the definitions of |
I'm trying to create a type that circularly references itself, but in a way that it makes sense - or at least it does to me. This is basically the same problem as @pelotom is having. type Data = number | string | Data[] | Record<string, Data>; If I modify it to the following, it works as intended, but this really is just more work for the user. This gets even worse when the data types are more complicated than a simple key-value pair. type Data = number | string | { [key: number]: Data } | { [key: string]: Data }; This type allows me to create values like the following and handle them in a type safe way even when they are deeply nested. const data: Data = {
foo: [ 1, 'one', { two: 2 } ],
bar: 'foobar'
}; @RyanCavanaugh Is there any other (sane) way to create type-safe nested objects? |
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed. |
I think this is a reasonable feature request for reasons that are made clear in the comments, can we reopen please? |
I think aleksey-bykov nailed the primary goal use case in this comment: type Json = null | string | number | boolean | Json[] | { [name: string]: Json } |
please stop bringing this silly "going infinitely deep" excuse, will you? we are all mature developers to know how to deal with it, aren't we? |
@brandonbloom the json problem was solved without recursive types, was posted somewhere in the issues |
|
come on, if you know you going deep you can prevent it, if you dont then you will see those overflows and thrn you know, it is not a stopper by any means or not an excuse moreover if a problem is so bad maybe it makse sense to add some static checks to see where tge problem is likely to arise? and why are so few new features to typescript been made lately, are you guys on a cut budget? |
Lots of summer vacations, some parental leave, slightly more staff turnover than usual, and mapped types had a substantial bug tail that's taken a lot of time. |
I appreciate the TypeScript team's hard work. Mapped types have been a great addition and well worth the added complexity and corner cases! |
Is it the same issue? playground // Error: Type alias 'DeepPartial' circularly references itself
type DeepPartial<T> = T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends Date
? T
: { [P in keyof T]?: DeepPartial<T[P]> }; upd: found working |
I also wonder if I hit this bug or if it is different (modified example from this Tweet): type FormValue<T> = { value: T }; // potentially many more fields
type FormModel<T> =
//T extends Array<infer U> ? { value: FormModel<U>[] } : // THIS WORKS
T extends Array<infer U> ? FormValue<FormModel<U>[]> : // THIS ERRORS
T extends object ? FormValue<FormModelObject<T>> :
FormValue<T>;
type FormModelObject<T> = {
[key in keyof T]: FormModel<T[key]>;
};
const model: FormModel<{
bar: string,
baz: {
foo: string
},
items: string[]
}> = {} as any;
model.value.bar.value.toLowerCase();
model.value.baz.value.foo.value.toLowerCase();
model.value.items.value.map(item => item.value.toLowerCase()); I wonder if there is a solution were I can re-use Update: Looks like I found a solution in the |
@tycho01 The code quoted by @Aleksey-Bykov is brilliant and it works; but only the first time. If I change the value of one of the tuples being concatenated, TypeScript hangs. Also adding this function to the bottom of the concat example will hang Typescript declare function foo<A, B>(a: A[], b: B[]): Concat<A,B> Example link: |
Hmm, I think I also ran into this issue when trying to implement a type that deeply removes
The hack mentioned earlier will not work for me (or I haven't figured it out yet), because I need to infer |
@bobvanderlinden Would the following satisfy your requirements? type GraphQLObject = { '__typename': string }
type DeepRequired<T> = T extends GraphQLObject ?
Required<{ [TKey in keyof T]: DeepRequiredObject<T[TKey]> }> :
(T extends Array<infer TItem> ? DeepRequiredArray<TItem> : DeepRequiredObject<T>);
interface DeepRequiredArray<T> extends Array<DeepRequired<T>> {}
type DeepRequiredObject<T> = {
readonly [K in keyof T]: DeepRequired<T[K]>;
} It seems to be producing the correct result in a simple case, but I'm unsure if it works in all required scenarios as I haven't yet used GraphQL myself. interface Result {
data: {
search: [
{
__typename: string,
name: string | null
}
]
}
}
const result: DeepRequired<Result> = {
data: {
search: [
{
__typename: "Human",
name: "Han Solo"
},
{
__typename: "Human",
name: "Leia Organa"
},
{
__typename: "Starship",
name: "TIE Advanced x1"
}
]
}
};
result.data.search[0].name; // string The implementation is largely based on @nieltg's |
@skreborn Wow, thanks for that one 😁👍. It's still a bit confusing why this variant does work, but directly inlining |
@bobvanderlinden This is, as far as I know, the only workaround for now. Inlining them doesn't work because circular referencing is not allowed. So why is it allowed if you put an extra step between the two? It's black magic that might have something to do with lazy evaluation, but it's definitely beyond my knowledge on the internal workings of the type system. |
You can sort of induce lazy evaluation by using the following pattern type DeepUnnest<P extends any[]> = {
'more': P extends Array<infer U>
? U extends any[]
? DeepUnnest<U>
: never
: never
'end': P extends Array<infer U>
? U
: never,
}[
P extends Array<infer U>
? U extends any[]
? 'more'
: 'end'
: 'end'
] Its ugly but it works avoiding circular reference problems. |
I found that cyclic references work very well with nested objects class Node<T> {}
type ExtractType<T> =
T extends SchemaNode<infer S>
? S extends { [name: string]: SchemaNode<infer _> }
? { [K in keyof S]: ExtractType<S[K]> }
: S extends Array<infer E>
? any[]
: S
: T; But as soon as I add an class Node<T> {}
type ExtractType<T> =
T extends SchemaNode<infer S>
? S extends { [name: string]: SchemaNode<infer _> }
? { [K in keyof S]: ExtractType<S[K]> }
: S extends Array<infer E>
? Array<ExtractType<E>>
: S
: T; Full source: class SchemaNode<T> {
constructor(private value: T) { }
validate(): ExtractType<SchemaNode<T>> {
return this.value as any; // [redacted]
}
}
type ExtractType<T> =
T extends SchemaNode<infer S>
? S extends { [name: string]: SchemaNode<infer _> }
? { [K in keyof S]: ExtractType<S[K]> }
: S extends Array<infer E>
? Array<ExtractType<E>>
: S
: T;
type AccountType = "retail" | "company";
const mySchema = new SchemaNode({
account: new SchemaNode({
token: new SchemaNode("john doe"),
type: new SchemaNode<AccountType>("retail"),
options: new SchemaNode({
autoLogin: new SchemaNode(true)
}),
isActive: new SchemaNode(true),
}),
history: new SchemaNode([
new SchemaNode("went to barber shop")
]),
credits: new SchemaNode(1000)
});
const res = mySchema.validate();
(res.account.token as string);
(res.account.isActive as boolean);
(res.account.type as AccountType);
(res.account.options.autoLogin as boolean);
(res.history as string[]);
(res.credits as number); |
Here's my work-around: type JsonPrimitive = string | number | boolean | null
interface JsonMap { [member: string]: JsonPrimitive | JsonArray | JsonMap }
interface JsonArray extends Array<JsonPrimitive | JsonArray | JsonMap> {}
type Json = JsonPrimitive | JsonMap | JsonArray |
I wanted to do this: type lineOfCode = [string, lineOfCode[], [string, void | number]]; But I had to do this workaround: type _lineOfCode = [string, unknown, [string, void | number]];
interface ILineOfCode extends _lineOfCode {
1: ILineOfCode[];
} Maybe this will help someone else. |
With #33050 the original example can now be written as:
|
Maybe that worked in a previous version but I'm getting this: |
Is it possible to write a tuple type that repeats a pattern? // [A] or [A, B, A] or [A, B, A, B, A] or [A, B, A, B, A, B, A] or ...
type PatternTuple<A, B> = [A] | [A, B, ...PatternTuple<A, B>]; |
This is working fine for me so far. *crosses fingers* type JSONValue =
| null
| boolean
| number
| string
type JSONArray =
| JSONValue[]
| JSONArray[]
| JSONMap[]
// Use interface because type errors:
//
// type JSONMap = Record<string, JSONValue | JSONArray | JSONMap>
// -> Type alias 'JSONMap' circularly references itself. ts(2456)
//
interface JSONMap { [key: string]: | JSONValue | JSONArray | JSONMap } |
@RebeccaStevens did you ever figure this out? I asked a related question: |
No. The best I came up with was: export type Alternate<A, B> =
| [A]
| [A, B, A]
| [A, B, A, B, A, ...Array<A | B>]; |
ah, I missed type NodeSnapshot =
| string
| [string, Attributes, ...NodeSnapshot] The above should have been: type NodeSnapshot =
| string
| [string, Attributes, ...NodeSnapshot[]] The circular reference error threw me off. Maybe it would make sense to change the error to: - Type alias 'PlaywrightNodeSnapshot' circularly references itself.
+ Type alias 'PlaywrightNodeSnapshot' circularly references itself. Did you mean to spread NodeSnapshot[]? |
Why does this work:
but this doesn't:
Shouldn't they be equivalent?
The text was updated successfully, but these errors were encountered: