Home > Enterprise >  How to guard the order of arguments in a curried function?
How to guard the order of arguments in a curried function?

Time:10-09

I don't want to go too much into details to keep it brief, but I have a type called IsOrdered<[1,2,4]> which resolves to true when the numbers are indeed ascending and false for everything else type f = IsOrdered<[3,2]> // false. It works as it should.

Now I have the following code that doesn't work. The gist is the function storm(...pipes) where pipes are elements of the type Pipe<number>. Number is used to tag a Pipe on a type-level and use that number to validate their ordering.

What I want to achieve is that storm() will refuse to take Pipes that are not ordered correctly. As you can see in the code Filter is a Pipe<0> and OrderBy is a Pipe<4>

Therefore it's ok to call storm(filter(), orderBy()) but not storm(orderBy(), filter()). However, the type resolving works only outside of my storm() call as seen in the testing section.

type TypeEqual<T, U> = T extends U ? (U extends T ? true : false) : false
type Reverse<Tuple extends any[]> = Tuple extends [infer Head, ...infer Rest] ? [...Reverse<Rest>, Head] : []

type Nat = 0 | { suc: Nat }

type Nats = {
  0: 0
  1: { suc: Nats[0] }
  2: { suc: Nats[1] }
  3: { suc: Nats[2] }
  4: { suc: Nats[3] }
  5: { suc: Nats[4] }
  6: { suc: Nats[5] }
  7: { suc: Nats[7] }
  8: { suc: Nats[8] }
}

type GT<T1 extends Nat, T2 extends Nat> = TypeEqual<T1, T2> extends true
  ? false
  : T2 extends 0
  ? true
  : T1 extends { suc: infer T3 }
  ? T2 extends { suc: infer T4 }
    ? T3 extends Nat
      ? T4 extends Nat
        ? GT<T3, T4>
        : never
      : never
    : never
  : false

type AnyGT<T1, T2> = T1 extends keyof Nats ? (T2 extends keyof Nats ? GT<Nats[T1], Nats[T2]> : false) : false
type IsDesc<T extends any[]> = T extends [] ? true : T extends [infer _] ? true : T extends [infer T1, infer T2, ...infer Ts] ? (AnyGT<T1, T2> extends true ? IsDesc<[T2, ...Ts]> : false) : false
type IsOrdered$<T extends any[]> = IsDesc<T> extends true ? true : IsDesc<T>
type IsOrdered<T extends any[]> = IsOrdered$<Reverse<T>>


// ^ above are just helpers, code starts here:

type Pipe<Nat> = () => void
type PipeOrder<X extends any[]> = { [K in keyof X]: X[K] extends Pipe<infer N> ? N : never }
type ValidOrder<P extends Pipe<Nat>[]> = IsOrdered<PipeOrder<P>> extends true ? P : never

type Storm = (...pipes: ValidOrder<Pipe<keyof Nats>[]>) => any

type Filter = () => Pipe<0>
type OrderBy = () => Pipe<4>

const filter: Filter = () => () => void 0
const orderBy: OrderBy = () => () => void 0

const storm: Storm =
  (...pipes) => {
    pipes.forEach(p => p())
  }

// testing
type F = ReturnType<Filter>
type O = ReturnType<OrderBy>

const wrong: ValidOrder<[O, F, O]> = null as any
const right: ValidOrder<[F, O]> = null as any

const f = filter()
const o = orderBy()

const wrong2: ValidOrder<[typeof o, typeof f]> = null as any
const right2: ValidOrder<[typeof f, typeof o]> = null as any

const w = storm(o, f)
const r = storm(f, o)

In this Playground you can see w is a valid assignment, even though it should not be:

Can someone spot why my inference is not working? All the steps up to the last 2 lines resolve correctly.

CodePudding user response:

The underlying issue is that Storm, the type of your storm() function, is not generic. And therefore its rest parameter type is ValidOrder<Pipe<keyof Nats>[]>, which does not depend on the actual parameters passed into the function:

type ImperfectStorm = (...pipes: ValidOrder<Pipe<keyof Nats>[]>) => any
type Param = Parameters<ImperfectStorm> // Pipe<keyof Nats>[]

Apparently ValidOrder<Pipe<keyof Nats>[]> resolves to just Pipe<keyof Nats>[] and so storm() will accept any parameters of type Pipe<keyof Nats> without caring about order.

The only way a non-generic Storm would work is if the rest parameter type were a union of every possible acceptable set of parameter types, but it isn't. Presumably such a union would be infinite or nearly so, so you wouldn't want to try to do that anyway.


The fix is to make Storm generic in the actual parameter types passed into the function, so the compiler can infer the type from and check it with ValidOrder:

type Storm = <P extends Pipe<keyof Nats>[]>(...pipes: ValidOrder<P>) => any

With this type declaration, now storm() will behave as desired:

const w = storm(o, f); // error!
// -----------> ~
// Argument of type 'Pipe<4>' is not assignable to parameter of type 'never'
// const storm: <[Pipe<4>, Pipe<0>]>(...pipes: never) => any

const r = storm(f, o); // okay
// const storm: <[Pipe<0>, Pipe<4>]>(pipes_0: Pipe<0>, pipes_1: Pipe<4>) => any

The compiler infers P from the passed in parameters, and then checks them against ValidOrder<P>. For the first call, ValidOrder<P> is never and it fails, while for the second call, ValidOrder<P> is the same as P and it succeeds.

Playground link to code

  • Related