@ George Madeley @ Personal Studies @ 8/3/24
This is a collection of notes that I, George Madeley, took when taking the Codecademy TypeScript course. I do not take ownership of the material covered and these notes should only be used for educational purposes.
-
TypeScript riles have the .ts extension.
-
We run our code through the TypeScript transpiler.
-
The transpiler often outputs a JavaScript version of our code if possible
TypeScript is a superset of JavaScript code.
The TypeScript transcompiler can be used on the command line by running the 'tsc' command.
When we declare a variable with an initial value, the variable can never be reassigned to value of a different data type. TypeScript recognises these data types:
-
Boolean,
-
Number,
-
Null,
-
String,
-
Undefined
An object shape describes, among other things, what properties and methods it does or doesn't contain. TypeScript will tell you if you're using a method hat is not associated with that object.
If a variable is declared without an initial value, it can be assigned and reassign dot any type.
We can tell TypeScript what type something is or will be by using a type of annotation.
let mustBeAString: string;
This declares the variable with type string without reassigning it a value.
The tsconfig.json file is always placed in the root of your project and you can customize what rules you want the TypeScript compiler to enforce.
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"stritNullChecks": true,
},
"include": [ "src/**/*.ts" ]
}
-
"compilerOptions" -- is a nested object that contains all the rules to enforce.
-
"target": "es2017" -- is telling the project to use the 2017 version of ECMAScript standards for JavaScript.
-
The project will be using "commonjs" syntax for importing and executing modules.
-
"StrictNullChecks", variables can only have null or undefined values if they are explicitly assigned those values.
Function parameters may be given type annotation with the same syntax as variable declaration
function greet(name: string) {
console.log(`Hello, ${name}!`);
}
We can also pass in optional values to a function:
function greet(name?: string) {
console.log(`Hello, ${name || "Anonymous"}!`);
}
We use the '?' operator at the end of a parameter name to tell TypeScript it is an optional parameter.
We can have default parameters where if a value is passed that is not undefined or the same type as the default value, the default value will be used:
function greet(name = "Anonymous") {
console.log(`Hello, ${name}!`);
}
If we set a variable to a function where the return of that function is of type string, the variable cannot be a different data type.
We can add an explicit type annotation after a functions closing bracket.
function greeting(name?: string): string {
return `Hello, ${name ? name : 'World'}!`;
}
It is also good practice to use type void on a function that does not return anything:
function greet(name: string): void {
console.log(`Hello, ${name}!`);
}
A documentation comment is denoted with the first line /** and a final line */. It's common for each line to start with *. We place a function documentation comment right above the declaration of the function.
We use @param to describe each of the function's parameters, and we can use @returns to describe what the function returns.
/**
* Calculates the sum of two numbers.
* @param a - The first number.
* @param b - The second number.
* @returns The sum of `a` and `b`.
*/
function getSum(a: number, b: number): number {
return a + b;
}
Below is an example of TypeScript array annotation.
let names: string[] = ['Alice', 'Bob', 'Charlie'];
An alternative method is to use the Array<T> syntax where <T> stands for type.
let names: Array<string> = ['Danny', 'Samantha', 'John'];
What about multi-dimensional arrays?
let arr: string[][] = [
['a', 'b', 'c'],
['d', 'e', 'f'],
['g', 'h', 'i']
];
What about arrays that have elements of different types?
let ourTuple: [string, number, boolean] = [ "Hello", 42, true ];
When an array is typed with elements of specific types, it's called a tuple. You cannot alter a tuple length and you cannot change element type.
Just so you ware aware, type inference always returns an array. When we want tuples, we need to use explicit type annotations.
let tup: [number, number, number] = [1, 2, 3];
let concatResult = tup.concat([4, 5, 6]);
In the example above, concat() returns an array. As a result, concatResult is an array allowing us to expand the tuple 'tup'.
The rest parameter syntax allows a function to accept an indefinite number of arguments as an array.
function smush(firstString, ...otherStrings) {
return firstString + otherStrings.join('')
}
The rest parameter can be type safe by using the same type annotation as an array:
function smush(
firstString: string,
...otherStrings: string[]
) {
return firstString + otherStrings.join('')
}
We use Enums when we'd like to enumerate all possible values that a variable could have.
enum Direction {
North,
East,
South,
West
};
let whichWay: Direction;
whichWay = Direction.North;
As you can see, the variable is of type direction and can only equal a value defined in the Enum.
But here's a fun fact: though the variable is set to North, it also has the value of 0 as North was the first in the Enum. If it was South, it would also equal 2.
If you want to change the starting number, use the following code.
enum Direction {
North = 7,
East,
South,
West
};
You can manually set each value to its own number.
We can also assign Enums string values.
- It is a common practice that the string value is a capitalisation of the Enum valise. I.e., north="NORTH"
Even though numeric Enums can be assigned numbers, string Enums cannot be assigned strings making them more secure.
The properties of objects have the same type annotation as normal variables:
let aPerson: {
name: string,
age: number
}
The above code states that aPerson can only be an object with those properties and type.
Type aliases are alternative type names that we choose for convenience:
type MyString = string;
let myVar: MyString = "Hello World";
This is helpful when we have long complicated types that are repeatedly use like object and array types.
Function types specify the argument types and return type of a function.
type StringToNumberFunc = (
arg0: string,
arg1: string
) => number;
Generics give us the power to define our own collections of object types:
type Familty<T> = {
parents: [T, T],
mate: T,
children: T[]
}
If T was equal to string:
let aStringFamily: Family<string> = {
parents: ['alice', 'bob'],
mate: 'charlie',
children: ['dave', 'eve']
}
In our code, which actually write 'T' in the type to use as a placeholder. We could declare a new variable with type Family<Boolean> and code would still work.
We can also use generics to create a collection of type functions.
function getFilledArray<T>(
value: T,
n: number
): T[] {
return Array(n).fill(value);
}
The above code creates an array of size n and fills it with value of type T. to invoke the function:
getFilledArray<string>('cheese', 3);
getFilledArray<boolean>(true, 7);
Unions allow a variable to be a value of two of more specified types. For instance, an employee ID variable could be of type number or string but not Boolean.
Unions allow use to define multiple type members by separating each type member with a vertical line.
let ID: number | string;
A type guard is a conditional that checks if a variable is a certain type
if (typeof margin === 'number') {
...
}
This is called type narrowing. Type narrowing is when TypeScript can figure out what type a variable can be at a given point in our code.
If a function returns multiple different types, TypeScript will infer that the function returns a union of those types. As a result, there is no need for us to manually infer this return type.
To create a union that supports multiple types of an array's value, wrap the union in parentheses followed by square brackets.
const timesList: (string | number)[] = [ dateNum, dateString, ];
When we put type members in a union, TypeScript will only allow us to use the common methods and properties that all members of the union share. Number and Boolean can both use .ToString but Boolean cannot use .toFixed(2).
This rule also applies to objects.
We can use literal types with unions:
type Color = 'green' | 'yellow' | 'red';
function changeLight(color: Color) {
...
}
If we tried to call changeLight('purple'), we would get an error.
A type guard is conditional which checks the type of a variable before the program performs actions on that variable unique to that type.
if (typeof data === 'string') {
...
}
Sometimes we want to see if a specific method exists on a type instead of a type like string. The in operator checks if a property exists on an object itself or anywhere within its prototype chain.
type Tennis = {
serve: () => void;
}
function play(sport: Tennis|Soccer) {
if ('serve' in sport) {
return sport.serve;
}
}
We can also use else statements to make our life easier.
If the if statement contains a return statement, then we don't even need an else statement.
Below is an example of an interface:
interface Mail {
postagePrice: number;
address: string;
}
const catalog: Mail = ...
Interface only works on objects.
The interface keyword in TypeScript is especially good for adding types to a class. Since interface is constrained to type objects and using class is a way to program with objects, interface and class are a great match.
interface Robot {
identifuy: (id: number) => void;
}
class OneSeries implements Robot {
identifuy(id: number) {
console.log('OneSeries Robot');
}
}
The implements keyword is then often used to apply and type Robot in OneSeries. OneSeries can have methods of its own but it needs to implement the methods and attributes of Robot.
Implements and interfaces allow us to create types that match a variety of class patterns.
To type an object nested inside another object, we could write an interface like this:
interface Robot {
about: {
general: {
id: number;
name: string;
}
}
getRobotId: () => string;
}
TypeScript allows us to compose types. We can define multiple types and reference them inside other types.
interface About {
general: General;
}
interface General {
id: number;
name: string;
version: Version;
}
interface Version {
versionNumber: number;
}
We can now read our code easier with named types and we can reuse smaller interfaces in other places in our code.
Sometimes it's convenient to copy all the type members from one type into another type. We can accomplish this with the extends keyword.
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
const mySquare: Square = { color: "blue", sideLength: 10 };
It's useful to write an object type that allows us to include a variable name for the property name. this feature is called index signatures. We may get data from an API that looks like this:
{
'40.712776': true;
'41.203323': true;
'40.417286': true;
}
We know that the property names will be strings and we will know the values will be Boolean, but we don't know what the property names will be. To type this object, we can utilize an index signature to type this object.
interface SolarEclipse {
[latitude: string]: boolean;
}
- The latitude name is purely for readability
TypeScript allows some methods and attributes within an interface to be optional.
interface OptionsType {
name: string;
size?: string;
}
As you can see, an optional type member is denoted with an ? before the : operator.