Typescript Note 1

I have been using typescript for over a year now and seeing how useful it is compared to vanilla javascript, I decided I should improve my typescript understanding to reap even more benefits out of it. In this series, I will include some notes from playing around and learning more about typescript.

Specification

Aims

  • Create a function that will add '1' to an input string or number.
  • Infer types correctly depending on the input.

Examples

  • addOne(1) should return 2
  • addOne('1') should return '11' (concatenated)

Typescript

version: 4.5.5

For my tsconfig.json, I referred to this blog post to figure out the strict options:

{
  "compilerOptions": {
    "module": "commonjs",
    "outDir": "out",
    "noImplicitAny": true,
    "removeComments": true,
    "preserveConstEnums": true,
    "sourceMap": true,
    "useUnknownInCatchVariables": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true
  }
}

Initial thoughts

Initially I wanted to learn more about generic types so that's what I started with. Since the spec deals with multiple types, I thought generic types might be of help.

Generic Type Functions

function genericFun1<T>(param: T): T {
  if (typeof param === 'string') {
    return param + '1' // Type 'string' is not assignable to type 'T'.
  }
  return param + 1
}

const genericFun1Arrow = <T>(param: T): T => {
  if (typeof param === 'string') {
    return param + '1'
  }
  return param + 1
}

const genericFun1_1 = genericFun1(1) // const genericFun1_1: 1
const genericFun1_2 = genericFun1('1') // const genericFun1_2: "1"
const genericFun1_3 = genericFun1(true) // const genericFun1_3: true

The function signature will take the type of its parameter and return the same type. This is done using type variables, which can be seen as the <T> in the function signature.

genericFun1Arrow is available just to show the difference in syntax between arrow and normal functions.

This function is actually not valid with my typescript configuration, the main issue being the error Type 'string' is not assignable to type 'T' . The input, param, is of type T which could be any type, but typescript will only allow strings to be added to strings. Since T may not be a string, the syntax is invalid.

In addition, the inferred types were inaccurate as boolean values should not be valid according to the spec. Also, the inferred types seem to be enumerated as seen in const genericFun1_1: 1, where I expected const genericFun1: number instead.

function genericFun2<T extends string | number>(param: T): T {
  if (typeof param === 'string') {
    return param + '1'
  }
  return param + 1
}
const genericFun2_1 = genericFun2(1) // const genericFun2_1: 1
const genericFun2_2 = genericFun2('1') // const genericFun2_2: "1"
const genericFun2_3 = genericFun2(true) // const genericFun2_3: string | number
// Argument of type 'boolean' is not assignable to parameter of type 'string | number'.ts(2345)

Changing the type parameter from <T> to <T extends string | number> helps to catch boolean values being sent used as a function parameter. The typescript compiler complains Argument of type 'boolean' is not assignable to parameter of type 'string | number'.ts(2345) for genericFun2_3.

Again this is function is invalid as the ambiguity of T has not been dealt with, and the inferred types seem to still be enumerated.

function genericFun3<T extends string | number>(
  param: T
): T extends number ? number : string {
  if (typeof param === 'string') {
    return param + '1'
  }
  return param + 1
}
const genericFun3_1 = genericFun3(1) // const genericFun3_1: number
const genericFun3_2 = genericFun3('1') // const genericFun3_2: string
const genericFun3_3 = genericFun3(true) // const genericFun3_3: string | number
// Argument of type 'boolean' is not assignable to parameter of type 'string | number'.ts(2345)

Next I tried using a conditional type. This still does not resolve the issue with the ambiguous T, but it did help to make the inferred types more expected as in genericFun3_1: number. Conditional types allow flexibility when defining the types.

After trying generic functions, I could not find the way to get my intended specification to work. Instead, it seemed function overloads might hold the answer.

Function Overloads

function overloadFun(param: string): string
function overloadFun(param: number): number
function overloadFun(param: string | number) {
  if (typeof param === 'string') {
    return param + '1'
  }
  return param + 1
}
const overloadFun_1 = overloadFun(1) // const overloadFun_1: number
const overloadFun_2 = overloadFun('1') // const overloadFun_2: string
const overloadFun_3 = overloadFun(true) // const overloadFun_3: string | number
// No overload matches this call.

Function overloads make it easy to define multiple function signatures, with a single function implementation. The inferred types when using this function is correct, and using a boolean value as input yields the message No overload matches this call., which is appropriate since none of the signatures account for booleans.

Hence, this is my accepted solution for now!

A note about arrow function overloads

I am more inclined to using arrow functions in my code, so I was wondering if function overloads could be done on arrow functions as well. Seems like I am not alone in wondering as seen in https://stackoverflow.com/a/53143568/2085381.

type IOverload = {
  (param: string): string
  (param: number): number
}
const overloadFunArrow: IOverload = (param: any) => {
  if (typeof param === 'string') {
    return param + '1'
  }
  return param + 1
}

Using an arrow function for the above function would look like this. The inferred types are correct since the function signatures are the same, however this syntax works only when param: any is used. This removes any type checking that is within the function body. Hence, I will continue to use normal functions when I require type safety in this situation.

Click here for the code used in this note.