0

I'm trying to define a function includes, that works for both, arrays and strings.

But I'm having trouble typing it correctly. If I understand correctly, overloading is possible using unions. So this is what I'm trying:

// Declarations
export function includes<T>(arr: T[], el: T): boolean;
export function includes(str: string, substr: string): boolean;
// Implementation
export function includes<T>(arr: T[] | string, el: T | string) {
  return arr.indexOf(el) !== -1;
}

But I'm getting the error:

'string' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype

which makes sense, because if arr is of type T, el shouldn't be of type string, but the unions makes that a "possibility".

So how would I correctly type the implementation here to say that the arguments may be either T[] and T or string and string?

6
  • I would type el as any in the implementation. It's not part of your public API anyway. Commented Jan 1, 2020 at 14:19
  • Okeey, certainly possible here, but assuming that the function would return T in case 1 and string in case 2, then I'd want to keep the type safety. So then I'd have to type it correctly. How would I do that Commented Jan 1, 2020 at 14:22
  • You could still use any as the return type. Commented Jan 1, 2020 at 14:23
  • Sure, but then I'd lose the type information Commented Jan 1, 2020 at 14:24
  • 1
    And if you want it all in one declaration: function includes<T>(a: T extends string ? string : T[], b: T): boolean ... Commented Jan 1, 2020 at 16:23

1 Answer 1

1

It's not safe to call arr.indexOf(el) where arr is T | string[] and el is T | string because the compiler isn't sure that you're not mismatching them, as in "typescript".indexOf(2) or [1,2,3].indexOf("rip"). The fact that the type of arr and el should be correlated is hard to express in TypeScript. In the absence of correlated record types (see microsoft/TypeScript#30581)], it's reasonable to do less type-safe things like use any or other workarounds inside the implementation, since the overload call signatures prevent any such unsoundness from being visible to callers of your function.

However, in your case, I'd be inclined to use the following implementation signature:

export function includes<T>(arr: { indexOf(x: T): number }, el: T) {
    return arr.indexOf(el) !== -1;
}

Here, all we care about is the fact that arr has an indexOf() method that takes a value of some type T and returns a number, and that el is a T. This makes arr.indexOf(el) compile with no error no matter what, and it's compatible with both of the overload call signatures. In fact, you might even want to ditch the overloads and just use the above definition, unless you have some reason why includes() should be restricted to string-or-Array<T> inputs. Let's make sure it behaves:

includes("typescript", "rip"); // okay
includes([1, 2, 3], 2); // okay

includes("typescript", 2); // error
includes([1, 2, 3], "rip"); // error

Looks good to me. Hope that helps; good luck!

Link to code

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.