Table of contents
Hi 👋, I'm back again with a new article! If you haven't read the previous one yet, you can check it out here. Grab a cup of coffee, tea, or whatever you prefer, and let's dive in! In the previous article, we had an overview of TypeScript compiling and how the syntax looks. Today, we're going to delve into something very critical in TypeScript: types. An oversimplified description of TypeScript would be JavaScript with added types. Of course, there's more to TypeScript than just types, but types are the core feature. We'll talk about:
Why to use types
Basic types
Never and void types
Literal types (I found this one particularly interesting)
Object types and more...
JavaScript is a weakly typed language. This means that the type of a variable is determined by the JS engine at runtime and can change from one type to another.
let x = 6; // JS: Ah, this is a Number.
let y = 12 / 6; // JS: Ok, this equals 2, so y is also a Number.
x = "I can be a string"; // JS: Now x is a string, no problem.
You might consider this a feature, but in certain situations, like when using APIs or working across multiple files with other developers, it can cause a severe headache. Imagine our friend Hossam wrote this simple function that adds a tax value:
export const addTax = p => p * 1.14;
Now, we import it and use it:
const shoppingList = [2, 1, undefined, 10];
const prices = shoppingList.map(p => addTax(p)); // [2.28, 1.14, NaN, 11.4] NaN?
This simple bug won't be discovered until runtime.
Let's now take a look at TypeScript types.
Types
In TypeScript, variables are defined as follows:
var | let | const variableName: type = value;
And the code we saw above wouldn't compile because, once you assign a type to a variable, you can't change this type:
let x: number = 6; // tsc: This is a number.
x = "I can be a string"; // tsc: No, you can't.
TypeScript offers us some basic types, called everyday types, such as:
string
number
boolean
any: any type
string[]: array of strings (could be any other value)
undefined & null
Additionally, there are some utility types:
Readonly: can't be reassigned
Record<Keys, Type>: key-value pair (like in maps)
You can also use union types (e.g., string | null
), meaning the variable can be a string or null.
Wait a minute—if a variable is a string, it can also be null. So why should I explicitly annotate it?
It's more readable and clarifies that this variable is allowed to be null.
Never Type
Something that was new to me in TypeScript, and may be new to you as well, is the Never
type. The Never
type is mostly related to functions' return types. It means that this function will NEVER return—not even void.
Look at the following examples:
const logging = message => console.log(message);
logging("I ❤️ TypeScript"); // "I ❤️ TypeScript" is logged into the console.
The logging
function logs the passed message to the console and then returns void (in other words, nothing). Now consider this function:
const loggingForever = message => {
while (true) {
console.log(message);
}
};
loggingForever("I ❤️ TypeScript");
The loggingForever
function will log the passed message, then log it again, then again... until your computer crashes or the sun explodes, whichever happens first XD. But it will NEVER return, so the TypeScript version of these functions will be as follows:
const logging = (message: string): void => console.log(message);
const loggingForever = (message: string): never => {
while (true) {
console.log(message);
}
};
// Another case where your function may `Never` return is when it throws an error.
const throwError = (errorMessage: string): never => throw new Error(errorMessage);
Do you know another case where a function's return type isnever
? Leave a comment.
Literal Type
Literal types are a more concrete subtype used to narrow down a wider type (e.g., 1 could be a subtype of the number
type).
"There are three sets of literal types available in TypeScript today: strings, numbers, and booleans. By using literal types, you can allow an exact value that a string, number, or boolean must have."
— TypeScript Handbook
An example of literal types is a function that simulates rolling a dice or flipping a coin:
const flipCoin = (): "head" | "tail" => return (Math.floor(Math.random() * 2) + 1) == 1 ? "head" : "tail";
The flipCoin
function will return only two values: "head" or "tail." Any other value will cause an error.
Of course, there's more to be said about literal types, but this article is just an introduction to all types. Are you still curious? Read the official docs here.
Object Types
Now, you may wonder how to add type annotations to objects. Here's a simple example of how it's done:
// JS way
let fullname1 = {
firstname: "Eslam",
lastname: "Ahmed"
};
// TS way
let fullname2: {
firstname: string,
lastname: string
} = {
firstname: "Eslam",
lastname: "Ahmed"
};
// or explicitly
type fn = {
firstname: string,
lastname: string
};
let fullname3: fn = {
firstname: "Eslam",
lastname: "Ahmed"
};
/* as you might expect, this will cause a compilation error:
"Property 'lastname' is missing in type '{ firstname: string; }' but required in type 'fn'."
*/
let fullname4: fn = { firstname: "Hossam" };
Let's Write in TypeScript
Enough talk about types for today. Now, I hope you're familiar with the basic types and functions in TypeScript. Why not apply what we've learned to writing a famous algorithm called binary search?
Binary search is a search algorithm with a time complexity of O(log(n)).
(If you're not familiar with Big O notation, O(log(n)) means that if you're searching in 1024 values, you're going to find the desired value, if it exists, in only log(1024) = 10 steps in the worst case.)
The algorithm's steps are simple:
Consider you have n sorted elements, the first in position 0 and the last in position n-1 in an array called values
, and you're searching for the value v
.
Declare two variables:
start = 0
,end = n-1
. The search range is [0:n-1] with length n.Let
pos = floor((end-start)/2)
.If
values[pos]
equalsv
, then return pos.If not, then check if
values[pos]
is less thanv
.
If that's true, change the search range tostart = pos
,end = n-1
.
But ifvalues[pos]
is greater thanv
, the range will bestart = 0
,end = pos
.Repeat from step 2 until
start = end
.
const binarySearch = (nums: number[], v: number): number => {
let start: number = 0;
let end: number = nums.length - 1;
while (start <= end) {
let pos: number = Math.floor((start + end) / 2);
pos;
if (nums[pos] === v) {
return pos + 1;
}
if (v > nums[pos]) {
start = pos + 1;
} else {
end = pos - 1;
}
}
// if v does not exist, return -1
return -1;
};
That's it for today! If you liked the content, give it a reaction,