VSjolund
VSjolund

Reputation: 36

Typescript generics - restrict function parameter and return type to same type and infer type from usage

I'm trying to restrict a function parameter to extend a function where parameters and return values are restricted to the same type, without requiring the function itself to be generic.

I have a simple factory function which creates dom nodes with attributes, like this:

function div(className, attributes, children)

EventListener attributes (such as onclick) are restricted to either a function or a tuple:

type EventHandler<T extends Event> = ((ev: T) => void) | [(ev: T, seed: SameType) => SameType, SameType];

Where SameType basically annotates that you should provide a tuple where the first item is a function which accepts a seed value, and a second argument which specifies the initial seed passed to the function once it is invoked.

I've tried to make the parameter itself be the generic, pass itself to a type and then using conditional matching, but with no luck. I cannot use generics on the interface itself as that would make no sense (onclick, oninput and onmouseover would all have different SameType seed values).

The function is inspired by Inferno's linkEvent function (https://github.com/infernojs/inferno/blob/master/packages/inferno/src/core/types.ts#L45), but instead of invoking a function you'd just pass in a tuple which I thought was handy, but the typing of it turned out a nightmare! Grateful for any input.

Is this issue related to the fact that Typescript disallows generics on the value level, or is there something I am missing? I'm thinking of this issue.

Upvotes: 0

Views: 1488

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250366

I'd stick to the function on each handler. Ensuring on the div function that the on* attributes have a tuple where the two elements are related in the way you want is possible, but not in a straight forward way.

It will involve capturing the actual type of the object literal passed into the div function and checking this custom constraint with a conditional type:

function div<TAttr extends {
  onClick:EventHandler<MouseEvent>,
  onKeyDown:EventHandler<KeyboardEvent>
}>(attributes: TAttr & CheckAttr<TAttr>): TAttr {
  return attributes;
}

type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never

type CheckAttr<T> = UnionToIntersection<CheckAttrHelper<T>[keyof T]> 
type CheckAttrHelper<T> = {
  [K in keyof T]:
    T[K] extends [(ev: Event, seed: infer P) => infer R, infer S] ?
      ([R] extends [P] ? [S] extends [P] ? never
          : Record<K, "Seed type is not assignable to parameter type">
          : Record<K, "Return type is not assignable to parameter type">
      ): never
}

type EventHandler<T extends Event> = ((ev: T) => void) | [(ev: T, seed: any) => any, any];

//ok
let a = div({
  onClick: [(ev, n: string | number) => 2, "1"],
  onKeyDown: [(ev, n: number) => n + 1, 1]
});

//err 
// Type '(string | ((ev: MouseEvent, n: string) => number))[]' is not assignable to type 
//'[(ev: MouseEvent, n: string) => number, string] & 
// "Return type is not assignable to parameter type"'.
let a2 = div({
  onClick: [(ev, n: string) => 2, "1"],
  onKeyDown: [(ev, n: number) => n + 1, 1]
});

Upvotes: 1

Related Questions