Search code examples
typescript

Why TypeScript complains on concatonation of string | number types with (+)


Trying to create a function that'll add 2 numbers or concatenate 2 strings like the following.

type Add = <T extends string | number>(a: T, b: T) => T;

const add: Add = (a, b) => {
  if (typeof a === 'string' && typeof b === 'string') {
    return a + b;
  }

  return a + b; // why is this error?, where we've 2 types & we've checked one so only one type is remaining
}
const result = add('a', 'b');

Playground Link

When there are string & number types and I've checked one type, why is TypeScript complaining about the other type?

// tsconfig.json
"compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "jsx": "preserve",
    "jsxImportSource": "solid-js",
    "allowJs": true,
    "noEmit": true,
    "strict": true,
    "noImplicitAny": true,
    "types": ["vinxi/types/client", "bun-types"],
    "isolatedModules": true,
    "paths": {
      "~/*": ["./src/*"]
    }
  }

Solution

  • If T can be string | number, then it is possible that a: string and b: number, or vice versa. TypeScript requires that you only add values of the same type, so you need to handle the merging of different types.

    The function declaration needs to be split into 3 cases.

    • In the first case, it receives two strings, and the result will be a string. (Actually, the first case is optional; I only wrote it for the sake of clarity. Since the third case, where the result of string | number is a string, covers this as well.)
    • In the second case, it receives two numbers, and the result will be a number.
    • The third case is your original case, BUT here the result will be a string instead of string | number.

    Arrow Function Syntax

    If you accept both values of the same type and also a mix of string | number. In the case of the mix, we know that we should expect a string result.

    type Add = {
      // If both a and b are strings, return a string
      (a: string, b: string): string;
      // If both a and b are numbers, return a number
      (a: number, b: number): number;
      // If one is a string and the other is a number, return a string
      (a: string | number, b: string | number): string;
    };
    
    const add: Add = (a: any, b: any) => {
      // If both a and b are numbers, return the sum as a number
      if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
      }
      
      // If either a or b is a string, return the concatenation as a string
      return (a.toString() + b.toString());
    };
    
    // Test cases
    const result1 = add(1, 2); // 3 (number)
    const result2 = add('a', 'b'); // "ab" (string)
    const result3 = add('a', 1); // "a1" (string)
    const result4 = add(1, 'b'); // "1b" (string)
    
    // Error cases
    const result5 = add(new Date(), 'b'); // error (ts2769)
    

    If you specifically accept only a and b of the same type:

    type Add = {
      // If both a and b are strings, return a string
      (a: string, b: string): string;
      // If both a and b are numbers, return a number
      (a: number, b: number): number;
    };
    
    const add: Add = (a: any, b: any): any => {
      // If both a and b are numbers, return the sum as a number
      if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
      }
      
      // If either a or b is a string, return the concatenation as a string
      return (a.toString() + b.toString());
    };
    
    // Test cases
    const result1 = add(1, 2); // 3 (number)
    const result2 = add('a', 'b'); // "ab" (string)
    
    // Error cases
    const result3 = add('a', 1); // error (ts2769)
    const result4 = add(1, 'b'); // error (ts2769)
    
    

    Function Declaration

    If you accept both values of the same type and also a mix of string | number. In the case of the mix, we know that we should expect a string result.

    function add(a: string, b: string): string;
    function add(a: number, b: number): number;
    function add(a: string | number, b: string | number): string;
    function add(a: string | number, b: string | number): string | number {
      if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
      }
    
      return String(a) + String(b);
    }
    
    // Test cases
    const result1 = add(1, 2); // 3 (number)
    const result2 = add('a', 'b'); // "ab" (string)
    const result3 = add('a', 1); // "a1" (string)
    const result4 = add(1, 'b'); // "1b" (string)
    
    // Error cases
    const result5 = add(new Date(), 'b'); // error (ts2769)
    

    If you specifically accept only a and b of the same type:

    function add(a: string, b: string): string;
    function add(a: number, b: number): number;
    function add(a: string | number, b: string | number): string | number {
      if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
      }
    
      return String(a) + String(b);
    }
    
    // Test cases
    const result1 = add(1, 2); // 3 (number)
    const result2 = add('a', 'b'); // "ab" (string)
    
    // Error cases
    const result3 = add('a', 1); // error (ts2769)
    const result4 = add(1, 'b'); // error (ts2769)