Function Overloading in TypeScript vs Using a Union Type

Viewed 4015

I am reading about function overloads. Essentially what they are is, you are creating let's say 3 functions, with the same name, that are passing 3 different parameters and return types. I am new to TS and I was wondering the following: wouldn't passing a union type and returning a union type be the same? Or is it something completely different?

This is what came up in my head as an example. Would this work or not?

Overload:

function f1(a: string) {
}
function f1(a: number) {
}

Using a union type:

function f1(a: string | number):string | number  {

}
3 Answers

Function overloading maps a specific input type to a specific return type. With a union you just know that the return is one of the valid types, but you lose the association between the input and the output. This may or may not be a problem depending on how and where you use the function. But that's the difference.

Here's what it looks like with overloads. The last line of the signature isn't one of the overloads, it describes the types for the implementation. Sometimes you'll see any, but you can also use the union here.

function overloaded(a: string): string
function overloaded(a: number): number
function overloaded(a: any): any {
    return a;
}

The different arguments return the specific type based on the overload that they match.

const oNum: number = overloaded(0);
const oStr: string = overloaded("");
const oBool = overloaded(true); //error

In our union, both input types just return the union, so we've lost the specificity.

function union(a: string | number): string | number {
    return a;
}
const uNum: string | number = union(0);
const uStr: string | number = union("");
const uBool = union(true); //error

There is a third option which is typescript generics. This allows us to keep the specificity while accepting infinitely many types. The boolean example works now. We are telling typescript "look at the type of the argument a and call that T". Then we get this type variable T which we can use in the return type. Here we just returning the same type T directly, but there is a lot more than you can do with it.

function generic<T>(a: T): T {
    return a;
}
const gNum: number = generic(0);
const gStr: string = generic("");
const gBool: boolean = generic(true);

Typescript Playground Link

function overloads can be more specific than union type and generic types for function argument types.

Real-world example:

The createActivity function is used to create three types of activities with different input parameters. CreateRenewalActivityParams, CreateInsureActivityParams and CreateTrafficGenerationActivityParams.

interface CreateActivityParams {
  activityCode: string;
  activityName: string;
  activityDesc: string;
  activityType: number;
  activityTypeName: string;
  startTime: string;
  endTime: string;
  paymentType: string | null;
}
interface CreateRenewalActivityParams extends CreateActivityParams {
  installmentCases: any[];
}

interface CreateInsureActivityParams extends CreateActivityParams {
  cases: any[];
}

interface CreateTrafficGenerationActivityParams extends CreateActivityParams {
  cases: any[];
}
export async function createActivity(data: CreateTrafficGenerationActivityParams): Promise<boolean>;
export async function createActivity(data: CreateInsureActivityParams): Promise<boolean>;
export async function createActivity(data: CreateRenewalActivityParams): Promise<boolean>;
export async function createActivity(data: CreateActivityParams): Promise<boolean> {
  return true;
}

// TSC throw error
createActivity({
  activityCode: 'test code',
  activityName: 'test name',
  activityDesc: 'test desc',
  activityType: 8,
  activityTypeName: 'test type name',
  startTime: '2021-07-20 00:00:00',
  endTime: '2025-07-20 23:59:59',
  paymentType: null,
  cases: [
    {
      pictureCode: 'test pic code',
      authDesc: null,
    },
    {
      pictureCode: 'test pic code',
      authDesc: null,
    },
  ],
  installmentCases: [],
});


export async function createActivityUnion(data: CreateTrafficGenerationActivityParams | CreateInsureActivityParams | CreateRenewalActivityParams) {
    return true;
}

// TSC pass
createActivityUnion({
  activityCode: 'test code',
  activityName: 'test name',
  activityDesc: 'test desc',
  activityType: 8,
  activityTypeName: 'test type name',
  startTime: '2021-07-20 00:00:00',
  endTime: '2025-07-20 23:59:59',
  paymentType: null,
  cases: [
    {
      pictureCode: 'test pic code',
      authDesc: null,
    },
    {
      pictureCode: 'test pic code',
      authDesc: null,
    },
  ],
  installmentCases: [],
});

export async function createActivityGeneric<Data extends CreateActivityParams>(data: Data) {
    return true;
}

// TSC pass
createActivityGeneric({
  activityCode: 'test code',
  activityName: 'test name',
  activityDesc: 'test desc',
  activityType: 8,
  activityTypeName: 'test type name',
  startTime: '2021-07-20 00:00:00',
  endTime: '2025-07-20 23:59:59',
  paymentType: null,
  cases: [
    {
      pictureCode: 'test pic code',
      authDesc: null,
    },
    {
      pictureCode: 'test pic code',
      authDesc: null,
    },
  ],
  installmentCases: [],
})

Now, we want the function's arguments to report a type incompatibility error when they contain both installmentCases and cases fields, because they belong to different activity types, which can be done using function overloads.

We can write three more specific overload signatures for the createActivity function with the above three parameter types and a general implementation signature with the CreateActivityParams type.

TypeScript Playground

Just extending @Linda Paiste 's comment about the generic types

const gNum: number = generic(0);

This is basically saying

function generic<0>(a: 0): 0;

Which might not be what you want at times. So when using generics, you want to pass in a type argument:

const gNum: number = generic<number>(0);
Related