#Defining type constraints for function var args

37 messages · Page 1 of 1 (latest)

somber rivet
#

Hi everyone, I'm trying to define types for a function I have defined a couple of months ago in javascript. The function itself accepts a series of arguments whose rule generally goes by:

  1. no. arguments must be even
  2. for every args[i] where i % 2 == 0 with type T there must be an args[i+1] having type (x: T) => any

This is what I've came up in a couple of hours. Even tho it makes sense to me logically, typescript does not seem to want it. The compiler complains: "Expected 0 arguments, but got 2.ts(2554)", seemingly not matching any type at all

type FunctionTaking<T> = (x: T) => unknown
type ValidPatternType = Record<string, any> | Record<string, never> | Array<unknown> | null | number | string
type FlattenPairs<T> = T extends [infer U, infer F, ...infer Rest]
  ? U extends ValidPatternType
    ? F extends FunctionTaking<U>
      ? Rest extends Array<unknown>
        ? [U, F, ...FlattenPairs<Rest>]
        : [U, F]
      : []
    : []
  : []

function matchArgs<T extends Array<unknown>>(...args: FlattenPairs<T>): void {
  console.log(args)
}

matchArgs(5, (test: number) => test) // Error: Expected 0 arguments, but got 2.ts(2554)
wet bridge
#

i suspect you're going to need to write out a bunch of cases up to some limit. does the real version of this function return a type that depends at all on the parameters?

somber rivet
#

Yes it does but i'ill take care of it after i get something to work on the parameters as it is what i care about the most

wet bridge
#

i ask because it's relevant to what strategy i suggest. you'll probably want to use overload signatures if the answer is "yes", otherwise you could just use a union type for the args

#

is having a fixed limit acceptable? how many args do you actually apply in reality?

somber rivet
#
const testObject = {
  firstName: 'foo',
  lastName: 'baz',
  points: [
    { x: 10, y: 12 },
    { x: 0, y: 1 }
  ],
  timeMetrics: {
    range: { from: 0, to: 100 },
    units: 'seconds',
    target: 12
  }
} 

const result = match(
  {
    firstName: String,
    lastName: 'baz',
    points: [{ x: 10, y: match.any }]
  },
  ({ firstName, points: [{ y }] }) => `${firstName} is at y: ${y}`,

  {
    firstName: String,
    lastName: match.any
  },
  ({ firstName, lastName }) => `${firstName} has last name: ${lastName}`,

  null, _ => 'default case'
)(testObject)
#

This is an example of the js library

wet bridge
#

if it's like a dozen then having separate cases might be reasonable. more than that it'll probably be too unwieldy

#

do you have the ability/desire to change the JS API at all? i think this could be done more easily via method chaining or even just by wrapping each pair into its own tuple

runic vortex
#

Here's a solution:

thorn harborBOT
#
nonspicyburrito#0

Preview:```ts
type ValidateArgs<T> = T extends [infer A, ...infer R]
? R extends [unknown, ...infer R]
? [A, (arg: A) => unknown, ...ValidateArgs<R>]
: []
: T

declare const fn: <const T extends unknown[]>(...args: ValidateArgs<T>) => {}

// should pass
...```

wet bridge
#

ah, i was assuming they wanted inferred function arg types

somber rivet
#

Yes, but that would be the easy way. I'd like to leave the syntax as it is, cause it looks familiar with other languages

runic vortex
#

Oh, do you want inferring the function type too?

somber rivet
#

Yep

#

Looks clean tho

wet bridge
#

you could mix my suggestion with @runic vortex's. have nice typing via overloads up to some limit, but support an arbitrary amount as long as they're annotated

runic vortex
#

Yeah I'm not sure if it's doable to infer function argument type (other than writing out a bunch of overloads), with explicit type though the solution I gave should work.

wet bridge
#

here's an example of the overload solution, in case it's not obvious:

thorn harborBOT
#
mkantor#0

Preview:```ts
function matchArgs<A, B extends (a: A) => unknown>(
a: A,
b: B
): void
function matchArgs<
A,
B extends (a: A) => unknown,
C,
D extends (a: C) => unknown

(a: A, b: B, c: C, d: D): void
function matchArgs<
A,
B extends (x: A) => unknown,
C,
D extends (x: C) => unknown
...```

wet bridge
somber rivet
#

Looks good

#

But I don't understand how @runic vortex 's one would NOT infer functions input params

runic vortex
#

If you are willing to change your API design, you can do this:

thorn harborBOT
#
nonspicyburrito#0

Preview:```ts
declare const fn: <A extends unknown[]>(
...args: {
[K in keyof A]: {
case: A[K]
then: (arg: A[K]) => unknown
}
}
) => void

fn(
{case: 42, then: arg => {}},
// ^?
{case: "hello", then
...```

wet bridge
#

yeah, that's similar to what i was thinking when i suggested wrapping the pairs in tuples

wet bridge
runic vortex
#

Oh yeah tuple is a good idea, also works and looks cleaner.

thorn harborBOT
#
nonspicyburrito#0

Preview:```ts
declare const fn: <A extends unknown[]>(
...args: {
[K in keyof A]: [A[K], (arg: A[K]) => unknown]
}
) => void

fn(
[42, arg => {}],
// ^?
["hello", arg => {}],
// ^?
[{foo: 42, bar: "hello"}, arg => {
...```

wet bridge
wet bridge
thorn harborBOT
#
mkantor#0

Preview:```ts
type CaseFn<A> = (a: A) => unknown

function matchArgs<A>(a: A, af: CaseFn<A>): void
function matchArgs<A, B>(
a: A,
af: CaseFn<A>,
b: B,
bf: CaseFn<B>
): void
function matchArgs<A, B, C>(
a: A,
af: CaseFn<A>,
b: B,
bf: CaseFn<B>,
c: C,
cf: CaseFn<C>
...```

somber rivet
somber rivet
# thorn harbor

But it does that only after typing in some valid expression, which I don't like too much. For the sake of simplicity I might go down this path even tho the predefined overloads idea is great too!

somber rivet
#
function match<A extends unknown[], R>(
  ...args: {
    [K in keyof A]: {
      when: A[K]
      then: (arg: A[K]) => R
    }
  }
): (arg: unknown) => R {
  return (arg: unknown) => args[0].then(undefined)
}

match(
  { when: 5, then: test => test.toString() },
  { when: 'string', then: v => v },
  { when: { name: 'a', surname: 'b' }, then: ({ name, ...other }) => name + `${JSON.stringify(other)}` },
  { when: [{ v: 'hello' }], then: ([{ v }]) => v },
  { when: 5, then: v => v }
)({})
#

i'd like { when: 5, then: v => v }to give a compilation error because it is not consisent with the return type of the other then functions