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 return2
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.