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
typeis'one'|'two'then yourdatamust be something that could be pushed to either list, henceIOne & ITwo.data: anyfor the third signature, you'll only be invoking the function via the first two anyway.add()as a "generic function". Instead maybe you want a term like "general"?