1

I want to make generic function for adding items to an array. I also want it to be type secured with intellisence etc.

I made smt like this:

interface IOne {
    id: number;
    text: string;
}

interface ITwo {
    id: number;
    data: string;
}

interface IItems {
    one: IOne[];
    two: ITwo[]
}

const list: IItems = {
    one: [],
    two: []
}

function add(type: 'one', data: IOne): void
function add(type: 'two', data: ITwo): void
function add(type: 'one'|'two', data: IOne|ITwo) {
    list[type].push(data)
}

but got error:

Argument of type 'IOne | ITwo' is not assignable to parameter of type 'IOne & ITwo'. Type 'IOne' is not assignable to type 'IOne & ITwo'. Property 'data' is missing in type 'IOne' but required in type 'ITwo'.

Fiddle

How to handle that?

5
  • If your type is 'one'|'two' then your data must be something that could be pushed to either list, hence IOne & ITwo. Commented Jun 2, 2021 at 13:30
  • I figured that out from error :) I want to know how to tell typescript that i have one or other i data Commented Jun 2, 2021 at 13:32
  • 2
    Do you mean "Assigning different argument to function"? Because, if so, I'd recommend using the whole word instead of this particular shorthand... Commented Jun 2, 2021 at 13:33
  • Just use data: any for the third signature, you'll only be invoking the function via the first two anyway. Commented Jun 2, 2021 at 13:33
  • 1
    Note that the term "generic" has a particular meaning in TypeScript, so you probably shouldn't refer to add() as a "generic function". Instead maybe you want a term like "general"? Commented Jun 2, 2021 at 14:39

1 Answer 1

3

This is fundamentally not something the TypeScript compiler can verify as safe. Each of type and data are of a union type, but the compiler will not be able to understand whether or not these unions are correlated; it will treat them as independent, and so it imagines that type might be "one" while data is ITwo, and complains that this is not safe to do.

There is an open issue at microsoft/TypeScript#30581 asking for support for dealing with such correlated unions, but it's mostly a warehouse to document when people run into the problem and to mention that the best thing to do is just to use a type assertion and move on.


The particular overload implementation you write actually is not type safe, since the implementation signature explicitly says that type and data are uncorrelated unions:

function add(type: 'one' | 'two', data: IOne | ITwo) {
  list[type].push(data); // error!
}

That's a reasonable error; even in principle, the compiler cannot know that list[type] will accept data. Since you happen to know from the call signatures that this will be fine, you can use a type assertion to suppress the error. For example:

function add(type: 'one' | 'two', data: IOne | ITwo) {
  (list[type] as (IOne | ITwo)[]).push(data); // okay
}

Here you've told the compiler that list[type] is an array whose elements can be either IOne or ITwo. This isn't technically true (neither list.one nor list.two should hold both IOne and ITwo elements), but is relatively harmless as long as you are certain that the implementation is correct. This is generally the case with type assertions; you are taking on some responsibility for guaranteeing type safety, since the compiler cannot.


If you wanted to actually have the correlation between type and data represented in the implementation (and thus you wouldn't even need the overloads), you could use a rest parameter whose type is a union of tuple types:

declare function add(...args: [type: "one", data: IOne] | [type: "two", data: ITwo]): void;

You can verify that this is only callable the way you want:

add("one", { id: 1, text: "" }); // okay
add("two", { id: 2, data: "" }); // okay
add("one", { id: 2, data: "" }); // error

But due to the limitation mentioned in microsoft/TypeScript#30581, the compiler still cannot follow the correlation in the implementation:

function add(...args: [type: "one", data: IOne] | [type: "two", data: ITwo]) {
    list[args[0]].push(args[1]); // error!
}

Here, at least, you could work around it by writing redundant code, to force the compiler to walk through the different cases for "one" and "two" via control flow analysis:

function add(...args: [type: "one", data: IOne] | [type: "two", data: ITwo]) {
  if (args[0] === "one") {
    list[args[0]].push(args[1]); // okay
  } else {
    list[args[0]].push(args[1]); // okay
  }
}

But such redundancy is annoying and does not scale well. I'd only suggest such things if having the compiler verify type safety is more important than developer productivity and writing idiomatic JavaScript. Instead, even in this case, the most reasonable solution is just to use a type assertion:

function add(...[type, data]: [type: "one", data: IOne] | [type: "two", data: ITwo]) {
  (list[type] as (IOne | ITwo)[]).push(data); // okay
}

which brings you back to something very much like the overload version. Oh well!

Playground 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.