Skip to content

Allow circular constraintsΒ #51011

Description

Suggestion

πŸ” Search Terms

Circular constraints, "Type parameter 'T' has a circular constraint.(2313)"

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Often constraints are truly circular, that is to say we want type-checking an parameter based on itself, for instance consider this...

declare const placeOrder: <T extends Order<T>>(order: T) => void

type Order<Self> =
  { ids: number[]
  , urgent: Self["ids" & keyof Self]
  }

declare const orderA: 1
declare const orderB: 2
declare const orderC: 3

placeOrder({ ids: [orderA, orderB], urgent: [orderA] })
// okay, good

placeOrder({ ids: [orderA, orderB], urgent: [orderC] })
// nope, `orderC` can't be marked as urgent as it's not being placed

Here the circular constraint T extends Order<T> compiles because it's not immediately circular, but in case of immediately circular the compiler complaints and doesn't allow compiling it...

type Basket = { bananas: number, apple: number }
declare const updateBasket: <T extends ShallowExact<T, Partial<Basket>>(basket: T) => void
//                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type parameter 'T' has a circular constraint.(2313)

updateBasket(({ banana: basket.bananas + 1, apple: 10 }))
// no error

type ShallowExact<T, U> =
  T extends U
    ? U extends unknown
        ? { [K in keyof T]: K extends keyof U ? T[K] : never }
        : never
    : U

As a workaround we could make the circular constraint non-immediate by using a variadic argument...

type Basket = { bananas: number, apple: number }
declare const updateBasket: <T extends [ShallowExact<T[0], Partial<Basket>]>(...basket: T) => void

updateBasket(({ banana: basket.bananas + 1, apple: 10 }))
// Type 'number' is not assignable to type 'never'

type ShallowExact<T, U> =
  T extends U
    ? U extends unknown
        ? { [K in keyof T]: K extends keyof U ? T[K] : never }
        : never
    : U

But this makes the type unnecessarily complicated, in some scenarios even more complicated...

type Basket = { bananas: number, apple: number }
declare const updateBasket:
  <F extends [(state: Basket) => ShallowExact<ReturnType<F[0]>, Partial<Basket>>]>(...f: F) => void

updateBasket(basket => ({ banana: basket.bananas + 1, apple: 10 }))
// Type 'number' is not assignable to type 'never'

type ShallowExact<T, U> =
  T extends U
    ? U extends unknown
        ? { [K in keyof T]: K extends keyof U ? T[K] : never }
        : never
    : U

This could have simply been the following if TS allowed circular constraints...

type Basket = { bananas: number, apple: number }
declare const updateBasket:
  <T extends ShallowExact<T, Partial<Basket>>(f: (state: Basket) => T) => void

updateBasket(basket => ({ banana: basket.bananas + 1, apple: 10 }))
// no error

type ShallowExact<T, U> =
  T extends U
    ? U extends unknown
        ? { [K in keyof T]: K extends keyof U ? T[K] : never }
        : never
    : U

So the feature request is to allow writing circular constraints.

Activity

  1. RyanCavanaugh commented on Sep 30, 2022

    @RyanCavanaugh
    Member

    A lot of this is caused by needlessly writing naked type parameters. For example you could equivalently write

    type Basket = { bananas: number, apple: number }
    declare const updateBasket: <T>(basket: ShallowExact<T, Partial<Basket>>) => void
    updateBasket(({ banana: basket.bananas + 1, apple: 10 }))
    // Error is found

    and similarly

    type Basket = { bananas: number, apple: number }
    declare const updateBasket: <T>(f: (state: Basket) => ShallowExact<T, Basket>) => void

    which would error on the following line if #241 were addressed.

    I'm not really aware of any coherent pattern that can't be expressed by either lifting out the constraint into the use site or adding an additional type parameter.

  2. devanshj commented on Sep 30, 2022

    @devanshj
    Author

    In those cases yes you could refactor like that, but how about this...

    declare const placeOrder: <T extends [Order<T[0]>]>(...order: T) => void
    
    type Order<Self> =
      Self extends number[] ? number[] :
      { ids: number[]
      , urgent: Self["ids" & keyof Self]
      }
    
    declare const orderA: 1
    declare const orderB: 2
    declare const orderC: 3
    
    placeOrder([orderA, orderB])
    // okay, good
    
    placeOrder({ ids: [orderA, orderB], urgent: [orderC] })
    // nope, `orderC` can't be marked as urgent as it's not being placed

    This can't be refactored to <T>(order: Order<T>).

    I know one won't need a circular constraint in everyday code but when one is writing rich DSLs that have dependent types, such circular constraints pop-up (eg here).

  3. Fireboltofdeath commented on Sep 30, 2022

    @Fireboltofdeath

    You could refactor that to <T>(order: Order<T>) by refactoring Order to not depend on itself. Circular constraints aren't necessary there.

    declare const placeOrder: <T extends number[]>(order: Order<T>) => void
    
    type Order<IDs extends number[]> = IDs | {
    	ids: IDs,
    	urgent: IDs[number][],
    }
  4. devanshj commented on Sep 30, 2022

    @devanshj
    Author

    How about this then...

    declare const placeOrder: <T extends [Order<T[0]>]>(...order: T) => T[0]
    
    type Order<Self> =
      Self extends number[] ? number[] :
      { ids: number[]
      , urgent?: Self["ids" & keyof Self]
      }
    
    declare const orderA: 1
    declare const orderB: 2
    declare const orderC: 3
    
    let order = placeOrder({ ids: [orderA, orderB], extraMeta: "hello" })
    // okay, good
    let test: string = order.extraMeta
    
    placeOrder({ ids: [orderA, orderB], urgent: [orderC] })
    // nope, `orderC` can't be marked as urgent as it's not being placed

    I think the thread is missing the point that here order's type is in principle a derivative of it's value, in some cases you can cleverly write types to not have a circular constraint, but that's not always possible and is quite besides the point.

    I know one won't need a circular constraint in everyday code but when one is writing rich DSLs that have dependent types, such circular constraints pop-up (eg here).

    As I said this placeOrder example is something I came up with randomly, if you want a real-world use-case then see the linked example. Perhaps a more minimal example is this, although here I was lucky that the circular constraint wasn't immediate and didn't had to use varargs (but if I refactor it to manually map the tuple instead of use a mapped type then the error comes up... https://tsplay.dev/w23RVN and the fix is to use a mapped type... https://tsplay.dev/mLybAW)

  5. nopeless commented on Sep 30, 2022

    @nopeless

    Devansh Jethmalani (@devanshj) It might be a design choice issue, but here is a quick hack for you

    // First you need this type generic
    type Items<Arr extends readonly any[]> =
      Arr extends []
        ? never
        : Arr extends readonly [infer first, ...infer rest]
          ? first | Items<rest>
          : never;
    
    type TypeGuard<T extends { ids: readonly any[] }, Ids> = {
      // Preserve for future use (might not be needed after 4.9 satisfies operator)
      ids: Ids,
      urgent: readonly Items<T["ids" & keyof T]>[],
    }
    
    type Base = {
      ids: readonly number[],
      urgent: readonly number[]
    }
    
    function constraintNeeded<T extends Base>(arg: T extends TypeGuard<T, infer _> ? T : never) {}
    
    // @ts-expect-error
    constraintNeeded({
      ids: [1,2,3] as const,
      urgent: [4] as const,
    })
    
    constraintNeeded({
      ids: [1,2,3] as const,
      urgent: [1] as const,
    })

    Note that this makes debugging types much harder (if you don't get it right the first try, type hints will not help you)

    However, with the new satisfies operator, these type of type guards might be a practical solution

    demo

  6. devanshj commented on Sep 30, 2022

    @devanshj
    Author

    Thanks but the code that you're trying to fix (the first in OP) already works perfectly fine (and is better than your version): https://tsplay.dev/m3X2bW

    I just showed this as an example of a (non-immediate) circular constraint TS is fine with.

  7. nopeless commented on Sep 30, 2022

    @nopeless

    Thanks but the code that you're trying to fix (the first in OP) already works perfectly fine (and is better than your version): https://tsplay.dev/m3X2bW

    I just showed this as an example of a (non-immediate) circular constraint TS is fine with.

    Smart, I must have skimmed too hard. Thanks for the example

  8. Fireboltofdeath commented on Oct 1, 2022

    @Fireboltofdeath

    in some cases you can cleverly write types to not have a circular constraint, but that's not always possible and is quite besides the point.

    I believe it's less about cleverly writing types and simply writing less complex types. For your placeOrder examples, it doesn't show a need for a circular constraint and having a circular constraints seems more confusing than not.

    Perhaps a more minimal example is this, although here I was lucky that the circular constraint wasn't immediate and didn't had to use varargs (but if I refactor it to manually map the tuple instead of use a mapped type then the error comes up... https://tsplay.dev/w23RVN and the fix is to use a mapped type... https://tsplay.dev/mLybAW)

    In this example, it seems like you're wanting to have custom arbitrary errors rather than an actual constraint which you can already enforce without using circular constraints by adding an intersection to the parameter. I don't think this method has any significant cons and is probably about as good clarity-wise.

    type Machine = <M>(m: M & InferStringLiteralTuple<ParseMachine<M>>) => M

    However, because you're using literal types, this won't allow you to throw custom error messages with plain strings like you currently do, you'd have to wrap the message in something else (e.g a single element tuple) or settle for never, but there is already an existing issue for addressing custom error messages which would allow you to throw any message regardless and may also be better suited for this use case. #23689

  9. devanshj commented on Oct 1, 2022

    @devanshj
    Author

    I think this thread has deviated from the main paint so here's a recap to keep the conversation extremely streamlined... Paraphrasing arguments and counter-arguments to keep it short.

    Argument (from Devansh Jethmalani (@devanshj) in #51011 (comment)):
    We can have circular constraints by using the varargs hack. But this makes the types unnecessarily complicated. So it'd be nice to have a more straightforward way.

    Counter-argument (from Ryan Cavanaugh (@RyanCavanaugh) in #51011 (comment)):
    But I don't know a circular constraint that can't be refactored into something that the compiler is happy with

    Argument (from Devansh Jethmalani (@devanshj) in #51011 (comment)):
    How will you refactor this one?

    declare const placeOrder: <T extends [Order<T[0]>]>(...order: T) => void
    
    type Order<Self> =
      Self extends number[] ? number[] :
      { ids: number[]
      , urgent?: Self["ids" & keyof Self]
      }

    Counter-argument (from fireboltofdeath (@Fireboltofdeath) in #51011 (comment)):
    By making the type of ids generic instead of the whole order...

    declare const placeOrder: <Ids extends number[]>(order: Order<Ids>) => void
    
    type Order<Ids> =
      | Ids
      | { ids: Ids
        , urgent?: Ids[number][]
        }

    Argument (from Devansh Jethmalani (@devanshj) in #51011 (comment)):
    I see, but what if I had to return the original order? How will you refactor this one?

    declare const placeOrder: <T extends [Order<T[0]>]>(...order: T) => T[0]
    
    type Order<Self> =
      Self extends number[] ? number[] :
      { ids: number[]
      , urgent?: Self["ids" & keyof Self]
      }
    
    let test: string = placeOrder({ ids: [1, 2] as [1, 2], extraMeta: "whatever" }).extraMeta

    This is the status quo, happy to hear a counter on this. I think this line of discussion would be the most productive, just my two cents.

  10. Fireboltofdeath commented on Oct 1, 2022

    @Fireboltofdeath

    Argument (from Devansh Jethmalani (@devanshj) in #51011 (comment)): I see, but what if I had to return the original order? How will you refactor this one?

    declare const placeOrder: <T extends [Order<T[0]>]>(...order: T) => T[0]
    
    type Order<Self> =
      Self extends number[] ? number[] :
      { ids: number[]
      , urgent?: Self["ids" & keyof Self]
      }
    
    let test: string = placeOrder({ ids: [1, 2] as [1, 2], extraMeta: "whatever" }).extraMeta

    This is the status quo, happy to hear a counter on this. I think this line of discussion would be the most productive, just my two cents.

    This doesn't really require much change besides adding a new generic since you want to infer an entirely new type for Order. The main point of my example was to pass the values you want to constrain against into the order rather than trying to fetch the same values afterwards as I believe that's more idiomatic and simpler than trying to constrain against a derivative of the inferred type variable.

    declare const placeOrder: <T extends Order<I>, I extends number[]>(order: T & Order<[...I]>) => T
    
    type Order<IDs extends number[]> = IDs | {
    	ids: IDs,
    	urgent?: IDs[number][],
    }

    Unfortunately, in this case, T itself doesn't allow inference for I but you can simply use the method I mentioned previously (intersecting the parameter) to add a candidate for I to be inferred.

  11. devanshj commented on Oct 1, 2022

    @devanshj
    Author

    simpler than trying to constrain against a derivative of the inferred type variable.

    Well, it's not, here again what you're doing is what I'd call a "workaround" to not have a circular constraint. What I would call simple is embracing the fact that the type of order depends upon it's value and hence there is a circular constraint by definition independent of code.

    I can increase the requirement even more, how would one refactor this... (I'm increasing the requirement slowly to keep it as minimal as possible.)

    declare const placeOrder: <T extends [Order<T[0]>]>(...order: T) => T[0]
    
    type Order<Self> =
      Self extends number[] ? number[] :
      { ids: number[]
      , urgent?: Self["ids" & keyof Self]
      , onChange?:
          Exclude<keyof Self, "ids" | "urgent" | "onChange"> extends infer MetaKey extends keyof any
            ? & { [K in MetaKey]?: () => void }
              & { [K in Exclude<keyof Self["onChange" & keyof Self], MetaKey>]?: never }   
            : never
      }
    
    declare const orderA: 1
    declare const orderB: 2
    declare const orderC: 3
    
    placeOrder({
      ids: [orderA, orderB],
      extraMeta: "hello",
      onChange: {
        extraMeta: () => {},
        // @ts-expect-error
        bogusProp: () => {}
      }
    })
    
    
    placeOrder({ ids: [orderA, orderB], urgent: [
      // @ts-expect-error
      orderC
    ] })

    Again I'm just trying to prove this claim of mine...

    in some cases you can cleverly write types to not have a circular constraint, but that's not always possible

  12. brad-jones commented on Oct 10, 2023

    @brad-jones

    If I'm totally honest the type algebra in TypeScript is hard for me to understand sometimes so I'm not sure if I have a similar use case or not, feel free to send me elsewhere if I'm off the mark with this. Anyway here goes, I'd like to do something like this:

    interface MyCustomDagDSL {
      jobs: Record<string, Job>
    }
    
    interface Job {
      needs?: JobName[];
      action: () => void
    }
    
    const dag: MyCustomDagDSL = {
      jobs: {
        foo: {
          action: () => console.log("foo did some work")
        },
        bar: {
          needs: ["foo"], // can I define JobName such that this array is typesafe?
          action: () => console.log("bar did some work")
        },
      },
    });

    Doing something like:

    interface MyCustomDagDSL {
      jobs: Record<string, Job<MyCustomDagDSL>>
    }
    
    interface Job<T extends MyCustomDagDSL> {
      needs?: (keyof T["jobs"])[];
      action: () => void
    }

    Still just gives me string.

    And then the next thing I tried was:

    interface MyCustomDagDSL {
      jobs: Record<keyof MyCustomDagDSL["jobs"], Job<MyCustomDagDSL>>
    }

    Which gives: 'jobs' is referenced directly or indirectly in its own type annotation
    After a little more messing about I ended up with:

    interface DagDSL<Self> {
      jobs: Record<keyof Self["jobs" & keyof Self], Job<DagDSL<Self>>>;
    }
    
    interface Job<T extends DagDSL<T>> {
      needs?: (keyof T["jobs"])[];
      action: () => void;
    }
    
    class Dagfile<T extends DagDSL<T>> {
      constructor(d: T) {
      }
    }
    
    new Dagfile({
      jobs: {
        foo: {
          action: () => console.log("foo did some work"),
        },
        bar: {
          needs: ["foo"], // yay this is now type safe
          action: () => console.log("bar did some work"),
        },
      },
    });

    But then I wanted to have 2nd level in my DSL, like this:

    interface DagDSL<Self> {
      jobs: Record<keyof Self["jobs" & keyof Self], Job<DagDSL<Self>>>;
    }
    
    interface Job<T extends DagDSL<T>> {
      needs?: (keyof T["jobs"])[];
      steps: Record<keyof T["steps" & keyof T], Step<Job<DagDSL<T>>>>;
    }
    
    interface Step<T extends Job<DagDSL<T>>> {
      needs?: (keyof T["steps"])[];
      action: () => void;
    }
    
    class Dagfile<T extends DagDSL<T>> {
      constructor(d: T) {
      }
    }
    
    new Dagfile({
      jobs: {
        foo: {
          steps: {
            step1: {
              action: () => console.log("foo did some work"),
            },
            step2: {
              needs: ["step1"], // not type-safe :(
              action: () => console.log("foo did some more work"),
            },
          },
        },
        bar: {
          needs: ["foo"],
          steps: {
            step1: {
              action: () => console.log("bar did some work"),
            },
          },
        },
      },
    });
  13. devanshj commented on Nov 1, 2023

    @devanshj
    Author

    I guess what you want is something like this...

    class Dagfile<T extends DagDSL<T>>{
      constructor(d: T){}
    }
    
    new Dagfile({
      jobs: {
        foo: {
          steps: {
            step1: {
              action: () => console.log("foo did some work"),
            },
            step2: {
              needs: ["step1"],
              action: () => console.log("foo did some more work"),
            },
          },
        },
        bar: {
          needs: ["foo"],
          steps: {
            step1: {
              action: () => console.log("bar did some work"),
            },
          },
        },
      },
    });
    
    type DagDSL<Self> =
      { jobs:
          { [J in keyof Prop<Self, "jobs">]:
              { needs?: Exclude<keyof Prop<Self, "jobs">, J>[]
              , steps:
                  { [S in keyof Prop<Prop<Prop<Self, "jobs">, J>, "steps">]:
                      { needs?: Exclude<keyof Prop<Prop<Prop<Self, "jobs">, J>, "steps">, S>[]
                      , action: () => void
                      }
                  }
              }
          }
      }
    
    type Prop<T, K> = K extends keyof T ? T[K] : never
  14. jcalz commented on May 22, 2026

    @jcalz
    Contributor

    F-bounded generics are cool when they work. I've definitely run into situations where the obvious approach of type Foo<T extends Bar<T>>> = β‹― or function foo<T extends Bar<T>>(β‹― fails and you have to start tap dancing to lift the constraint out somewhere.

    I don't quite understand why this is really considered a "circularity", though. If you have T extends Bar<T>, then either T is manually specified, in which case Bar<T> can just be evaluated to check the constraint; or T is inferred, which would be circular if constraints were inference sites. But they're not, so T just gets inferred from wherever, and then again, Bar<T> can just be evaluated to check the constraint.

    The only thing I can think of off the top of my head that is circular is what happens when inference fails and T falls back to the constraint. Then that really would be circular and lead to Bar<Bar<Bar<β‹―>>>, but at that point inference has failed, so presumably never or any or Bar<any> would be fine, right? Once inference fails you just get something that hopefully results in a meaningful error message somewhere, right?

    So what am I missing? Is there some other scenario where circular constraints actually results in a real problem?

  15. RyanCavanaugh commented on May 22, 2026

    @RyanCavanaugh
    Member

    but at that point inference has failed [...] Once inference fails you just get something that hopefully results in a meaningful error message somewhere, right?

    Failed inference doesn't mean an error is occurring, oddly enough. The most straightforward example is

    type SomeType<T> = { x: T };
    function foo<T extends SomeType<T>>(x?: T) {}
    foo(); // Not an error

    or where this occurs silently during overload selection.

    Is there some other scenario where circular constraints actually results in a real problem?

    Are you asking why it's per se unimplementable, or why we don't just remove the error? The former isn't the case; the latter prevents a stack overflow.

  16. jcalz commented on May 22, 2026

    @jcalz
    Contributor

    Ah, right, so failed inference for recursive constraints can already fall back to something "weird". The type checker is like "T is now SomeType<unknown>. Yes, that violates the constraint T extends SomeType<T>. What's the problem?"

    Could you say more about where the stack overflow would happen? Especially given that apparently failed recursive constraints tend to just get unknown for the inner reference? Where is the circularity if the error were to be removed? I suppose if I'm really interested I could try it and see for myself.

  17. RyanCavanaugh commented on May 22, 2026

    @RyanCavanaugh
    Member

    I suppose if I'm really interested I could try it and see for myself.

    This is how I would answer the question too πŸ˜…

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions