3

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?

2
  • Not sure what you want to enforce here, just that all 3 arguments are of the same type ? What type safety are you looking for from MergeFunction ? Commented Jun 19, 2018 at 17:29
  • There are multiple instances of these MergeFunctions. One takes three strings and returns a string, one takes three numbers and returns a number, etc. However, when passed around, it's generalized as a MergeFunction, not its implementation. The goal here was to be able to pass this general type around, and it would still work the same regardless of how it acted underneath. That is, merge(1,2,3) returns a number while merge('foo','bar','baz') returns a string, etc. Commented Jun 19, 2018 at 18:17

1 Answer 1

10

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!

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.