TypeScript From The Ground Up - 1
TypeScript From The Ground Up
The first language I used outside of academia was Python. I was fortunate enough to use Python after type hints were introduced, so I received help from a very dynamic and easy to grasp language and a type system to help me along the way. When I first started using JavaScript I missed having types to help reduce common bugs. I started using TypeScript almost at once because it could give me the type system I needed. But I used TypeScript's concepts out of necessity, so I only learned small bits along the way, mostly through searching for implementations of what I needed. This led to an incomplete understanding of TypeScript and its capabilities. Stepping back and relearning TypeScript has helped, and I think it will help you too. So, lets jump in.
How TypeScript Works
TypeScript is an extension of JavaScript, a superset. That means anything that can be done in JavaScript can be done in TypeScript. To do this, the TypeScript you write is translated, or transpiled, into JavaScript that you can run in a browser or a server. This means that the features of TypeScript, like types, are erased and the resulting JavaScript code does not have any TypeScript functionality.
Types
In contrast to other Languages with Types that focus on Classes, like Java, typescript is mostly concerned with ensuring that a value exists when you expect it to. This has to do with JavaScript's Objects. Essentially, when does this object have these methods and properties? Does this set of properties have the property I am looking for? Is it possible that my values have changed types when I get to a certain part of the code?
Common Types
Like Java or C#, JavaScript has the concept of Primitive types, or types that inherent to the language, like string, number, and boolean. These primitive types allow us a baseline of functionality and the ability to describe more complex types by grouping primitives together into sets, or Objects in JavaScript.
const name: string = "Drew"
const age: number = 42
const isGnar: boolean = true
JavaScript also has the types String, Number, and Boolean, but you shouldn't use those as they are Objects, and they don’t always behave as you would expect them to.
Any
If a type is not defined and TypeScript can't infer a type, then TypeScript will mark something as any. Any is a designation that turns off type analysis and checking for that property. Outside of prototyping, there are few cases where Any
is the best type to use. There are TypeScript compiler settings to throw errors when an implicit any is produced.
const name: string = "drew";
name.toUpperCase() // DREW
const age: any = 42;
age.toUpperCase() // Will fail at runtime. Numbers don’t have a `toUpperCase` method.
Other types
JavaScript has some newer types, both BigInt and Symbol, introduced in ES2020 and ES2015 respectively. BigInt allows really large numbers, and Symbol allows some nifty functionality, but both are rarely used.
Arrays
TypeScript has two formats for defining arrays, []
and Array<type>
notations. Using the Array<t>
notation does limit a few potential bugs, as we will see.
const catNames: Array<string> = ["Smokey", "Mooney", "Peaches", "Tinkerbell"];
const numbersAndString: Array<number | string> = ["Peaches", 1];
function addName(items: string | number[], newItem: string | number) {
return items.concat(newItem);
}
for(const name of catNames){
console.log(name);
}
catNames
is easy, it’s an array of strings. numbersAndString
is an array that can have numbers and strings, we will cover the |
syntax in a bit.
Let’s look at the first argument for addName
, what’s the possible mistake of string | number[]
? What we meant to say is that we will accept an array that can have both strings and numbers, but that is not what was described. What was described was items
can be either a single string or an array of numbers, the correct notation would be (string | number)[]
and using precedence to fix our issue.
Or we could use Array<string | number>
and reduce our chance of a small error occurring.
for(const name of catNames) {
console.log(name);
}
TypeScript is adept at implicit type assignments and knows that if catNames is an array of strings, then name must be a string. It is important to balance your use of explicit type declarations and letting TypeScript figure them out for you. This allows easier refactoring and reduces cognitive load.
//Mixed up implicit values
const mixedUp = ["a", 2, false, {name: "Drew"}];
Functions
With TypeScript you can define a functions return type, and the types of parameters. TypeScript will do its best to figure out a function’s return type.
function hello(name){
console.log(`Hello ${name}!`);
}
function aBetterHello(name: string){
return `A much better tomorrow, ${name}`;
}
TypeScript knows that hello
doesn’t return anything, so hello
s return type is void
. aBetterHello
does not have a return type, but typescript knows the return type of string interpolation is a string and implicitly marks the function as such. TypeScript will mark incorrect use, but that won’t prevent you running the code. As long as the code evaluates fine when the JavaScript runs.
console.log(aBetterHello(2)); // 'A much better tomorrow, 2'
Optional Properties
You can mark parameters as Optional, with one important note.
function setup(useDefault?: boolean);
function teardown(log: boolean | undefined);
setup(); // no params needed
teardown(); // a boolean or undefined must be supplied
The types of both functions first parameter equate to boolean | undefined
but TypeScript knows that a value for log
must be supplied, so will show an error if you try to call teardown
with no parameters.
Anonymous Functions
TypeScript can usually figure out the type of anonymous functions tied to iteration methods like forEach()
.
catNames.forEach(name => {
console.log(name.toUpperCase()) //Since catNames is an array of strings, name must be a string.
})
Objects
In JavaScript, Objects are a collection of properties. You can provide type information for individual objects, or let TypeScript try to infer the type.
const cat = {
name: "Smokey", //Must be a string
age: 10, //A number
};
const rock: {color: string} = {
color: "Grey", //Explicitly color must be a string
};
You can also designate optional properties, but that means optional properties can be undefined.
const catWithNoName: {color:string, name?:string} = {
color: "Orange",
};
catWithNoName.name.toLowerCase() // Error, name could be a string, or undefined, so can’t call `toLowerCase`().
Types
We have been working with the existing types, but what happens when we want to define our own types and be able to use and reference them throughout our application? TypeScript has Type
and Interface
to work with this. Creating these types is called aliasing. Let’s create an alias for a primitive and an object.
type cat = {
name: string,
age: number,
}
const smokey: cat = {
name: "Smokey",
age: 10,
}
console.log(smokey.age) // "10"
In TypeScript a type
is a way to describe a certain layout of values. Types applied to objects are very useful because you now have a way to provide context and a soft contract. TypeScript types differ from most type systems by only checking that the incoming value contains the required properties.
type Dog = {
name: string,
age: number,
color: string,
}
const snoopy: Dog = {
name: "Snoop",
age: 1,
color: "White",
}
function onlyCatsAllowed(cat: cat){};
onlyCatsAllowed(snoopy); // Valid
The function onlyCatsAllowed
wants to be given an object that has the name
and age
properties. Other objects that contain at least the properties defined will be accepted.
Interfaces
Types cannot be appended to after they have been declared.
Prefer Interfaces over types, until you need the additional functionality that types provide.
Literal Types
When you assign a number or string to a constant, you create a literal type. This is possible because the type and value are known and cannot change, but you cannot assign a type to the variable, or you lose that specificity.
const ten = 10; // 10
const five: number = 5; // Number
ten
has the value ten because as a constant it cannot be assigned anything else. However, five
has the type of number, because even though we assigned the value 5 to it, we gave it the type of number, and TypeScript will take the larger type over the specific type.
By itself this is not particularly useful, but what if you wanted to restrict something to a set of specific values?
Union Type
type notAString = string;
const myString: notAString = "I am a proud number";
console.log(myString.toUpperCase()) // "I AM A PROUD NUMBER";
Types for single values, like a string, are not entirely useful, but when used with unions it can be very beneficial.
//Union literals
type seasons = ("spring" | "summer" | "fall" | "winter");
let mySeason: seasons = "" //Invalid, "" not in seasons.
let youSeason: seasons = "spring" //Valid
Unions allow us to say, the value is going to be this, or this. It is common for functions to take similar objects as well as have multiple return types. In other languages, polymorphism is a way to handle this issue. Define multiple functions with the same name but different parameters or returns. JavaScript does not have polymorphism; it just returns values, and you are expected to interact with them. With TypeScript we can use unions to define this.
type Lizard = {
name: string
}
type Bird = {
name: string
}
function sayHi(pet: Lizard | Bird){
console.log(`My name is ${pet.name}`);
}
const aLizard: Lizard = { name: "Slithers" };
const aBird: Bird = { name: "R-2323B" };
sayHi(aLizard); // My name is Slithers
sayHi(aBird); // My name is R-2323B
You have to be careful in how you define unions with expanded types. You may want to change the seasons type to be accept any known season or any user supplied string like the following.
type seasons = "spring" | "summer" | "fall" | "winter";
const mySeason: seasons | string; // Type is string
TypeScript is unable to provide specific literal type hints for any value with type seasons
the best it can infer is that mySeason
is a string. This is because TypeScript looks at all the options in seasons
and knows the other values are strings and reduces the entire type to string
.
It’s also important to know, that if a value is a union, the properties available to the value are only properties that exist on both types.
function numberOrString(param: number | string){
param; //Can only access methods that are available to numbers and to strings
}
Conclusion
This introduction to TypeScript will set us up for more complex features in further lessons. Reading about new concepts is only half of learning something new. Create a sample project to work on in the meantime and try out what you learned. If you do that then the new ideas will stick with you better.