Nothing is more frustrating than having the Typescript compiler complain about something that obviously worked fine in Javascript, what the hell man!?
However, as frustrating as it seems, Typescript is actually trying to protect us from… Well, ourselves!
Sometimes we know more than Typescript and we can safely ignore such warnings, for example by declaring any
, but what if we want specific types, at least for the code to be self documenting?
Let’s consider the following Javascript example of a simple binary search:
function search(nums, target) {
let start = 0;
let end = nums.length - 1;
while (start <= end) {
const mid = Math.floor((start + end) / 2);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
start = mid + 1;
} else {
end = mid - 1;
}
}
}
Seems innocent enough, right?
And probably it works. For the most part!
How Typescript save us from ourselves
Lets take another look at the example above and try to type annotate it!
function search(numbers: number[], target: number): number {
let start = 0;
let end = numbers.length - 1;
while (start <= end) {
const mid = Math.floor((start + end) / 2);
if (numbers[mid] == target) {
return mid;
} else if (numbers[mid] < target) {
start = mid + 1;
} else {
end = mid - 1;
}
}
}
Going from a functional, working piece of Javascript to strong types with Typescript, seems pretty straight forward!
Well done!
However, you may soon meet the following error:
Function lacks ending return statement and return type does not include 'undefined'.ts(2366)
Again, what the hell man?!
As it turns out, the Typescript Language Server (the interpreter that runs in your editor) figured out that this method may actually return undefined
, a case we have completely forgotten to check for.
So even while it doesn’t seem that way, Typescript have entirely protected us from ourselves and it’s now possible for us to fix this, seemingly, functional function.
Fixing the problem
So how exactly do we fix this?
There are several things we can do to fix it.
In its current state we are making a few assumptions that Typescript doesn’t like:
- We assume that the given input is always valid
- We assume it will always return a number
These two assumptions in combination, is what Typescript doesn’t like. As developers, we may very well know that the input is always valid, a case of “we know more than Typescript”, which is totally fine! So the most obvious solution would be just saying “hey typescript, I know what I’m doing, so I’m overwriting the type here”, something like this:
function search(numbers: number[], target: number): any {
...
}
For brevity, I have omitted the functions content, as it isn’t important for this interpretation.
As you see know, we specify the return type as any
and we can safely continue using the function as we wanted.
Perhaps not the most desirable solution, so let’s see what another alternative may look like!
function search(numbers: number[], target: number): number {
let start = 0;
let end = numbers.length - 1;
while (start <= end) {
const mid = Math.floor((start + end) / 2);
if (numbers[mid] == target) {
return mid;
} else if (numbers[mid] < target) {
start = mid + 1;
} else {
end = mid - 1;
}
}
return 0;
}
In this example we simply added return 0;
after the loop ends. It could also be -1
or a similar value that you know won’t work.
This is a case of intentionally returning an errornous output, without throwing an error, that we can handle from the calling method.
Better already! But I think we can do even better.
Finally, I present what I think is the best – and most correct – version of this code-block, fully annotated in Typescript:
function search(numbers: number[], target: number): number|undefined {
let start = 0;
let end = numbers.length - 1;
while (start <= end) {
const mid = Math.floor((start + end) / 2);
if (numbers[mid] == target) {
return mid;
} else if (numbers[mid] < target) {
start = mid + 1;
} else {
end = mid - 1;
}
}
}
Note here I use a union type and I tell the compiler “this method will either return number
or undefined
“.
This moves the responsibility for handling the error, to the place where we call the method.
This leaves us with a clear and minimal function that does what it’s supposed to, while knowing it won’t throw an error, but cannot be called with improper input and at the same time ensures that we handle the undefined
outcome in any place we call it.