Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Assigning arrow function to generic function type with TypeScript

I've done some digging on similar questions but I cannot find a solution that works. I've got some types of generic functions, but I cannot seem to implement them correctly.

In short, I have this:

/** Takes three values of the same type and collapses them into one */
declare type MergeFunction = <T>(def: T, file?: T, arg?: T) => T

/** The implementation I'm having trouble with. Merge three arrays of the same type. */
const arrayMerge: MergeFunction = <T>(def: T[], file: T[] = [], arg: T[] = []): T[] => [ ].concat(def).concat(file || [ ]).concat(arg || [ ])

However, I get a compiler error:

Property 'arrayMerge' is incompatible with index signature.
  Type '<A>(def: A[], file?: A[], arg?: A[]) => A[]' is not assignable to type 'MergeFunction'.
    Types of parameters 'def' and 'def' are incompatible.
      Type 'T' is not assignable to type '{}[]'.

How do I actually implement this type?

like image 386
CynicalBusiness Avatar asked Dec 18 '25 23:12

CynicalBusiness


1 Answers

As you've defined it, a function of type MergeFunction must work for any type T that the caller specifies. So arrayMerge is not a MergeFunction, since it only accepts arrays. Here's one way to implement your MergeFunction as specified:

declare type MergeFunction = <T>(def: T, file?: T, arg?: T) => T;
const returnLastSpecifiedThing: MergeFunction = <T>(def: T, file?: T, arg?: T) =>
  typeof arg !== 'undefined' ? arg : 
  typeof file !== 'undefined' ? file : 
  def;

In fact, the only thing you can safely do when implementing a type like MergeFunction is to return one of the inputs, because you don't know anything about T since the caller is in charge of that. There's certainly no way to be sure that T is an array.


Perhaps you mean for MergeFunction to be a type where the implementer chooses the generic parameter T. In this case, you can make the type generic instead of the function:

declare type MergeFunction<T> = (def: T, file?: T, arg?: T) => T;

Note how the <T> moved from the function to the type. The original definition is a specific type alias which refers to a generic function type, while the new definition is a generic type alias which, when you plug in a value for T, refers to a specific function type. (Sorry if that's confusing.) It is now much easier to implement some specific type of this. For example:

const concatenateStrings: MergeFunction<string> = 
  (def: string, file?: string, arg?: string) =>
    def + (file ? file : "") + (arg ? arg: "");

The function concatenateStrings is a MergeFunction<string>.

At this point it seems like it should be simple to represent arrayMerge as some kind of MergeFunction<>. Unfortunately it isn't. TypeScript lacks the sort of generics you need here. What you want to say is something like:

const arrayMerge: <T> MergeFunction<T[]> = // invalid syntax
  (def: T[], file: T[] = [], arg: T[] = []): T[] =>
    ([] as T[]).concat(def).concat(file || []).concat(arg || []);

But you can't do that directly (as the linked issue describes). The closest you can get is to add a layer of indirection, such as a function call:

const makeArrayMerge = <T>(): MergeFunction<T[]> =>
  (def: T[], file: T[] = [], arg: T[] = []): T[] =>
    ([] as T[]).concat(def).concat(file || []).concat(arg || []);

Now makeArrayMerge is a function that, when called with a specified type parameter T, produces a MergeFunction<T>. This works, but is harder to use (and doesn't infer types the way you'd like):

const numArray = makeArrayMerge<number>()([0, 1, 2], [3, 4, 5]);

Oh well, that's the best I can do given the limitations of TypeScript generics. It's up to you to decide if you really need the above indirection or if some specific array type will work for you. Hope that helps. Good luck!

like image 155
jcalz Avatar answered Dec 20 '25 17:12

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!