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.