
Dynamic Typing is So Easy
In JavaScript, a variable's "type" is dynamic, meaning it can change as its value changes. For example,
1let pi = 3.14; // pi is a number
2pi = 'ratio of circumference to diameter'; // pi is now a string
In the following CodePen, we assign age to the common JS primitives (string, number, boolean, object, and undefined) to show that JavaScript is totally fine with switching types.
But this freedom comes with some costs. In the example above, age is used as a number, string, boolean, object and even undefined. If we know age to be a specific type (e.g. number) throughout our application, we can make use of fancy functions like calcAgeNextYear().
1function calcAgeNextYear(age) {
2 return age + 1;
3}
4
5let age = 25;
6console.log(`Next year, I will be ${calcAgeNextYear(age)}`);
What if we change
1age
1age = '25 years'; 2console.log(`Next year, I will be ${calcAgeNextYear(age)}`); 3
JavaScript tries to play nice with all data types. Using the example above, let's see what happens when we pass in other data types.
As you can see, JavaScript tries to figure out how to handle the various age
values being passed into calcAgeNextYear()
. But this is probably not what we want from the method. It expects age to be a number.
Enter Typescript
Typescript is JavaScript, but with types! This means that we can type our variables, function parameters, object properties, etc.
What are "types"?
Remember back in your math classes when you were introduced to sets? Here is a easy definition of a set: "What is a set? Well, simply put, it's a collection ... it is just things grouped together" (source)
Types (in Typescript) are just sets; they are a way of describing a group of "things". For example, a string
is a set of all possible combinations of letters. A number
is the set of all possible real numbers (between some min and max and to some order of accuracy.) And boolean
is the set of true
and false
.
Type your variables?
Typescript lets you "type" your variables so that only that type can be used as a value for that variable.
"Big deal!" you might say. "This just adds more work to my code."
The "extra work" will save you hours of debugging and catch your mistakes before you even run your code. Let's upgrade our calcAgeNextYear()
function so the age
argument is a number
.
1function calcAgeNextYear(age: number) {
2 return age + 1;
3}
4
5calcAgeNextYear(25); //?=> 26
6
7calcAgeNextYear("25 years old"); //ERROR
8calcAgeNextYear(true); //ERROR
9calcAgeNextYear([25]); //ERROR
Now, if we try to pass anything besides a number to calcAgeNextYear()
we will get an error! Our program will not compile. Typescript tells us: "Argument of type 'string' is not assignable to parameter of type 'number.'"
Inferring Types
In our example, we instantiate a variable called age
with a value of 25.
let age = 25;
Typescript sees that you are giving this variable a numeric value, and it automatically types age
as a number. Hovering over age
(in your editor) gives you information about the variable's type.
let age: number
This is the syntax for typing a variable.
let name: string = "Sara";
Here, we explicitly give name
a type of string (even though Typescript will do it automatically)
Inferring types is not just for variables. Let's look at our calcAgeNextYear()
function. If we hover over the function, we get this information:
function calcAgeNextYear(age: number): number
This says that calcAgeNextYear
has a numeric parameter (age
) and returns a number. Typescript is able to figure out what type the function returns by looking at its code.
Just Scratching the Surface
There is so much more to Typescript, but before moving on, let's look at how you can start coding in TS.
Playground
The easiest way to get started coding in Typescript is to use the Typescript Playground. This online editor allows you to both write and run your TS code. It also shows you the resulting JavaScript code. That's right. Typescript "compiles" into JavaScript.
The Playground also has examples that will help you explore future topics such as Union and Intersection Types.
To get started, check out this playground which builds an object from three input parameters. Try running the code.
1function buildStudentObject(name: string, age: number, enrolled: boolean) {
2 return {
3 id: Math.floor(Math.random() * 1000),
4 school: "Hallpass Academy",
5 name,
6 age,
7 enrolled
8 }
9}
10
11const student = buildStudentObject("Gabby Jones", 22, true);
12
13console.log(student);
If you hover over the variable student
, you can see what type it is. Similarly, you can hover over buildStudentObject()
to see its type. Because it is a function, its type includes both typed input parameters and a return type.
What is the type of the function console.log()?
New Types
In the learning check above, you were introduced to three new types.
any
Typescript allows you to tightly type your code, but sometimes, you don't know what the type will be.
Consider JSON.stringify()
. What type will this function return? It is impossible to tell without looking at the input and even then, there is an infinite number of types that it could return. Enter any
. any
is TS's way of saying that the type can be anything; we are not limiting it to any single type.
Be careful; before typing something as any
, think if that is the best solution or is just a lazy solution.
You can learn more about any
in this playground.
[]
- square brackets
Appending square brackets to a type means it is an array of that type. here are some examples:
const names: string[] = ["Gabby", "Jerome", "Z"];
const ages: number[] = [22, 10, 68];
const data: boolean[] = [true, false, false];
void
A function or method that does not return anything, is given a "return type" of void
.
function noop(): void {}
const noop: () => void = () => {};
Both of these no-op functions do not return anything. Note the difference in how you identify the return type for these two ways of creating a function.
npm i -D typescript ts-node
Another way to get started is to develop locally using your favorite editor (e.g. VS Code). At your command prompt (shell), you will enter the following...
1mkdir my-first-ts-project
2cd ./my-first-ts-project
3npm init -y
4npm i -D typescript ts-node
5npx tsc --init
The important part is in the two modules that are being installed. The first is typescript which will allow you to compile your TS code into JavaScript. The second is ts-node which allows you to run node with typescript code.
The last command creates a Typescript configuration file (tsconfig.json) - more on that in future posts.
Now, open your editor and create your first .ts file! Of course, you must start with hello-world.ts
1function random(arr: string[]) {
2 return arr[Math.floor(Math.random() * arr.length)];
3}
4
5const greetings = ["Hello", "Howdy", "Hey", "Hi"];
6const targets = ["World", "Earth", "Galaxy", "Universe"];
7
8const buildMessage = (greeting: string, target: string) => {
9 return greeting + " " + target + "!";
10}
11console.log(
12 buildMessage(
13 random(greetings),
14 random(targets)
15 )
16);
Notice that we did not have to explicitly type greetings
or targets
. TS inferred that they are string arrays (string[]
). Similarly, we did not have to give the two function explicit return types. TS infers the the return type which is why we can pass random()
as string arguments for buildMessage
.
Earlier, we worked with objects but did not ever explicitly type a variable as an object. Create a new file students.ts with this code:
1type Student = {
2 name: string;
3 age: number;
4 enrolled: boolean;
5}
6
7function generate() {
8
9 const names: string[] = ["Gabby", "Jerome", "Z", "Luz", "Rosa", "Alex", "Joel"];
10
11 const name = () => {
12 return names[Math.floor(Math.random() * names.length)];
13 }
14 const age = (lowerLimit: number = 18, upperLimit: number = 99) => {
15 return Math.floor(Math.random() * (upperLimit - lowerLimit)) + lowerLimit;
16 }
17 const enrolled = () => {
18 return Math.random() > 0.35;
19 }
20 const student = () => {
21 return {
22 name: name(),
23 age: age(),
24 enrolled: enrolled()
25 } as Student;
26 }
27
28 return {
29 name,
30 age,
31 enrolled,
32 student
33 }
34}
35
36function display(student: Student) {
37 console.log(student);
38}
39
40const MAX = 10
41for (let i = 0; i < MAX; i++) {
42 display(generate().student());
43}
There is a lot going on here.
Type Alias
First off, we define a Student
object type that has three properties: name which is a string, age which is a number, and enrolled which is a boolean. We call Student
a "type alias" because we can now use Student whenever we want to refer to the specified object type. It is a shortcut (alias) for the type.
We could create alias of the types used on the Student properties.
type Name = string;
type Age = number;
type Enrolled = boolean;
type Student = { name: Name, age: Age, enrolled: Enrolled };
I am not sure this adds anything to our code, but it gives you an idea of how type aliases are define.
"Unnecessary Type Assertion"
In the generate() function, we initialize a local variable to hold the possible names of students.
const names: string[] = ["Gabby", "Jerome", /*...*/ ];
We could have written the same statement without including the type...
const names = ["Gabby", "Jerome", /*...*/ ];
In both instances, names
is an array of strings. Many will advocate that the second (where TS infers the type) is a better practice; after all, it is less typing.
I argue that when defining collections like this, it makes sense to explicitly type them. That way, you will see an error when you later add the name 411 (instead of "411").
Type Coercion
Sometimes we want to give TS a hint as to the type of some value. We coerce the value's type. (Also known as type casting).
Notice that in the function to generate a random student (student()
), the return statement ends with as Student;
This is telling TS to treat the object being return as a Student
object. The signature for student()
becomes:
const student: () => Student
Without this, the function signature would have been:
const student: () => { name: string; age: number; enrolled: boolean; }
Actually, these two types are the same (Student
is an alias for the object type). So it really isn't necessary to cast the return value as Student
, but it does help as we will see in just a bit. Meanwhile, try removing the as Student
type coercion to verify that TS has no problem interpreting one for the other (as showing when we pass the result of student()
into the function display()
that is expecting a Student
argument.
Careful when Coercing
Important: If you removed the as Student
type coercion, please add it back in.
Time goes by, someone on your team realizes that there needs to be some way of uniquely identifying a student. So, that someone, adds an id property to Student
:
type Student = {
id: number;
name: string;
age: number;
enrolled: boolean;
}
Your program still runs but notice the output is missing the new id property. Makes sense since we did not add it. But why didn't Typescript throw an error. That is why we are using TS, right?
The problem lies with type coercing/casting. In our student()
function, we have told TS that what we are returning is a Student
. Essentially, we are saying that we know better and that TS should trust us.
Typescript does a bit of checking to see if the value is like a Student
, but does not verify that it is exactly a Student
with all of the properties. For example, try changing student()
to:
const student = () => { return "Sara" as Student; }
Typescript will complain with the following error message:
"Conversion of type 'string' to type 'Student' may be a mistake because neither type sufficiently overlaps with the other."
Since { name, age, enrolled } "sufficiently overlaps" with Student
, TS is fine with us coercing the incomplete object into a Student
.
Better Approach
A better approach would be to type the student() function:
const student: () => Student = () => { /* same fn body */ }
Try changing your code to this...
1 const student: () => Student = () => {
2 return {
3 name: name(),
4 age: age(),
5 enrolled: enrolled()
6 };
7 }
Now Typescript give you an error on your function: "... Property 'id' is missing in type ..."
Let's fix this problem and try our program.
1const student: () => Student = () => {
2 return {
3 id: Math.floor(Math.random() * 1000),
4 name: name(),
5 age: age(),
6 enrolled: enrolled()
7 };
8 }
Now we get student objects with ids.
Takeaway?
The lesson is that we should use type coercion carefully and only when there are no other options.
Next Steps
We have only just introduced Typescript. There is lots more to learn.
Resources
The obvious place to go for more TS information is to the source, typescript.org, where you can find some wonderful Getting Started resources including:
Typescript.org also has a wonderful handbook that is a comprehensive look at TS with some basics, best practices and advanced topics.
Video Series
There are a two video series that I highly recommend:
- No BS TS by Jack Herrington. I like the way Jack approaches tutorials, and his TS series includes everything you need plus some bonus videos on patterns (series 2).
- Advanced Typescript by Matt Pocock. Matt offers a fantastic look at "advanced" features in Typescript starting with Generics - a vital core concept to any pro TS coder.