Advanced TypeScript Exercises - Answer 3
Tags: typescript, javascript
Published: 2020-02-19 11:37:16

--- series: Advanced TypeScript Exercises ---

The question I have asked was how to type function arguments in order to have dependency in them, so if first is `string` then second needs to be `string`, never mixed, the original code snippet:
```ts
function f(a: string | number, b: string | number) {
    if (typeof a === 'string') {
        return a + ':' + b; // no error but b can be number!
    } else {
        return a + b; // error as b can be number | string
    }
}
f(2, 3); // correct usage
f(1, 'a'); // should be error
f('a', 2); // should be error
f('a', 'b') // correct usage
```
There is not one possibility to solve the puzzle. Below few possible options.

## Solution 1 - Simple generic type for both arguments ##

```ts
function f<T extends string | number>(a: T, b: T) {
    if (typeof a === 'string') {
      return a + ':' + b;
    } else {
      return (a as number) + (b as number); // assertion
    }
  }
// but usage is disappointing:
const a = f('a', 'b'); // och no the return is number | string :(
```
Its nice and simple, we say we have one type for both arguments, therefor if first is `string` second also needs to be `string`. At the level of function api this is good solution, as all invalid use cases are now removed. The small issue here is need of assertion to number in `else`. But the big issue is, our return type is incorrect, it is not narrowed as we suppose it should 😪.

### Fixing return type
```ts
function f<T extends string | number, R extends (T extends string ? string : number)>(a: T, b: T): R {
  if (typeof a === 'string') {
    return a + ':' + b as R;
  } else {
    return ((a as number) + (b as number)) as R;
  }
}
const a = f('a', 'b'); // a is string :)
const b = f(1, 2); // b is number :)
```
As you can see solution is not so trivial and demands from us quite a lot of typing and assertion. We introduce here conditional type `R` which is now return type of our function, unfortunately we need to assert every return to this type. But the interface of the function is now perfect, arguments are type safe, and the return properly narrowed.

## Solution 2 - Compose arguments into one type
```ts
// type guard
const isStrArr = (a: string[] | number[]): a is string[] => typeof a[0] === 'string'

function f(...args: string[] | number[]) {
   if (isStrArr(args)) {
     return args[0] + ':' + args[1];
   } else {
     return args[0] + args[1]; // no assertion
   }
 }
```
This solution doesn't need even generic types. We compose our arguments into one type `string[] | number[]`. And it means that all all arguments will be string or all will be numbers. Because of no generic is used, there is no need of any assertion in the code. The issue is only fact that we need to provide additional type guard as pure condition doesn't narrow the type in `else`.  The issue can be considered as using indexes instead of `a, b` directly, and this we cannot pass, we can destructure in `if` and in `else`, but this would not be any better. Why we cannot - because checking separately `a` would not effect type of `b`. Consider:
```ts
function f(...[a,b]: string[] | number[]) {
  if (typeof a === 'string') {
    return a + ':' + b; // b is number | string
  } else {
    return a + b; // error both are number | string
  }
}
```
> There is issue with type `string[] | number[]`, it doesn't fully fit this function. Can you spot why, and what type would be better?

Also in this solution as we are not able to fix the return type, as we don't have generic type 🙄, it means return will always be `string | number`

## Solution 3 - Generic compose type for arguments
```ts
// type guard
const isNumArr = (a: string[] | number[]): a is number[] => typeof a[0] === 'number'

function f<T extends string[] | number[], R extends (T extends string[] ? string : number)>(...args: T): R {
  if (isNumArr(args)) {
    return args[0] + args[1] as R;
  } else {
    return args[0] + ':' + args[1] as R
  }
}
const a = f('a', 'b'); // a is string :)
const b = f(1, 2); // b is number :)
```
Solution 3 is similar to how we have fixed solution 2 by introducing return type `R`. Similarly here we need to also do assertion to `R` but we don't need to assert in `else` to `number`. As you can see what I did here is nice trick, I reversed condition and I ask firstly about numbers 😉.

## Solution 4 - function overloads
```ts
function f(a: string, b: string): string
function f(a: number, b: number): number
function f(a: string | number, b: string | number ): string | number {
  if (typeof a === 'string') {
    return a + ':' + b;
  } else {
    return ((a as number) + (b as number));
  }
}
const a = f('a', 'b'); // a is string :)
const b = f(1, 2); // b is number :)
```
By using function overloads we are able to create wanted arguments correlation, and proper return type. Overloads don't need generic types and conditional ones. **IMHO overloads in that case are the simplest and the best solution.**


## Summary - all those solution are not ideal ##

In summary I want to say - don't do that, if you can, don't create such functions, much better would be to create two different functions, one working with `string`, and one with `number`. This kind of ad-hoc polimorhism we made here, maybe makes somebody happy, but it creates only complication. More about that in [Function flexibility considered harmful](https://dev.to/macsikora/function-flexibility-considered-harmful-447n).

The code for this answer can be found in the [Playground](https://www.typescriptlang.org/play/index.html?target=99&ssl=1&ssc=1&pln=100&pc=2#code/PTAEGIEZQZwSwLYAcA2BTUBzNA7NAnOAY1ABUBYAKAG8rRQAzAVxyIBc4B7HRgHlNBoAHm1wATGLDaEcmUAB9QOJggBGBAHwAKAIYAuMgBpQqg6QCUoWpXr04DUFrYBPJGk4OdoALy-QAchhpOFl-S2tbW3w0NiZ8Hi8AagC9f1Bk1QBuOlsAX0EUGAwIyNBo2PjHLx1JZTUCS2StVVAapRV1fHNM0BBWmCL8Dm4c+lyc8ZtGLQAmYwBmbt6wIk58aPZQJhgdbByGLUhjfx0wnr6YAAtOJhQxEwwCfDX9rRP-Yxmli+vb+-VBOsXlMDu9jqowstQKt1mhNttdmgcqscEFWj5pmCAhDvmBOERLkpOKA2JcMOU4jw4LUOgQFFIZHI9FoqJM+uBQJAAHTQBhwIQksllGKUkmuJE0fYsdhcHgMfiCETiSRBRn0uqdYwAJUVohwEkcAmEeoNqpCcgA-AzzaADBqGtp9EYTGZzAYdSU7A4nOKPOjfN4AmbQuFRlERZUkik0hl+qAtdkpmMCkUrGH6BTKlpdHH7V10o4Wm08+ZLG0E2HJmNXnNQItzis1hs2FsdnsQYdjqdcbBfncHoDnvhXlivg3ezd+wCnsD6KDTuDIX0Yc3W4jkdw0V5A-OPtizlCvNTrbJbeYN6iWy0d53QGOoS1j3mz6yqFR2XfQABhTjITipnR8EwFRcDYSQQlAbgMBcNwqBKFE0WpABlaQAEF1gxXQDGDTAAG0AF11VpfACLddFjxwgifA0MU3D9HRcIABkIgMg2CUIpVYYY5S0Lk+MAzAYGw9i8MIxQ81ItMk1AexHGQtD1l0ICYFLKTSmFCoEmUpjCOSfxUgLASYFwyB8MTSJ8jQQpinTDTRSMnTDO00zxxwYkakGbjKwmGsFh7Fc4RbBF2znW93h7K5J3+R4gWHDtRwivtosHWdMQXfdLGXJtArXELoU3FttzSj5-BxKF8UJNzBXJCMqRpep8HpHDbRZShJnfMAORmHlQAAdUuZxQAAdwwPA0HuNhiW2aChUA-AdGcODOJlbhpj4rlcJ0QxVHw4TGSo8TiMkz0ZO9GD3E8Hw-ECETIROjNavRPSDIycdH3qzompEysUxs6SHs0p6THHGdGtUThSVaaJ2gar7GW8qYq2mWt6yhAL4TbCVQqOAJu3HSK-gHUGR3S+8fiionYpJvdSqXRtYQx9c2rfSgP3mUAACEYlERqQgYAhcCIDAdH1UB0AGfpPNlSRVEG7A8EIEgBJAnAwNoiV4IKmSYAAORUdDGsDLCT1EoiGtIgwjw+ggqO8GjzvoxzWP8PN-BZudpW4vgjSVfUVREg6Yc6AjtV1ZVDTDv2Taoq1mrtYjzG0dajNdd01NsWStGpPWEANpTBNU+67MjbTmKcwSTMI8tzLyX708iTMtIrsvnpjKGK9MuMtQR6sOxR-zssZvKDhx8L8aSymh2pz5Eop6cqamBDCoxXdFx7K2TZfRetevaYcbJsB3qDuk9HPZnKA60AkM4FAmC9gAWUAADFPdlSCADcCBQTgdAkJaQVfqtA4TocLGFMCbMiOFlpe2AfHBqYC4GdDInmaBb9YGb0OvAl0GDj6NUgSJM2n0TqZwdpdZ2OE7q2UbkDfSbcsg-SsqmIu1DszVGtvmJoRZ2Glhrr3PhyM-LjnRkFTGrxR54yhATKcMUp7xVJrPQm89ZGhSxLTTK9NVzBSxvlS8-pipr3HBvOOZ96BLxMCvW8B9zFPmIlvXIQA).

**This series is just starting**. If you want to know about new exciting questions from advanced TypeScript please follow me on [dev.to](https://dev.to/macsikora) and [twitter](https://twitter.com/macsikora).