question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItĀ collects links to all the places you might be looking at while hunting down a tough bug.

And, if youā€™re still stuck at the end, weā€™re happy to hop on a call to see how we can help out.

Allow circular constraints

See original GitHub issue

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.

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:11 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
nopelesscommented, Sep 30, 2022

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

0reactions
devanshjcommented, Oct 1, 2022

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

Is it acceptable to have circular foreign key references\How to ...
Just declare the constraints as deferrable. But I do agree: in almost all cases circular references are a bad design.
Read more >
Remove or allow a circular reference - Microsoft Support
Find and remove a circular reference. You can also learn about the circular reference warning message and iterative calculation in Excel.
Read more >
Constraints & Circularities - Sequelize
Adding constraints between tables means that tables must be created in the database in a certain order, when using sequelize.sync.
Read more >
why does typescript not allow circular references in generics?
TypeScript does allow circular references in generic interfaces and ... circularity is not constrained to recursive tree-like objects:
Read more >
How to deal with a cyclic foreign key constraint using ...
In simple terms, a cyclic foreign key can be closely related to circular key references. Let's assume we have the following two tables:...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found