A Journey into TypeScript: My Learning Notes
My TypeScript learning notes.
Static type language, which can detect type errors at compile time. Specifically designed for the client-side, but JavaScript can run both on the server-side and client-side.
- Alternative to JavaScript (a superset)
- Allows us to use strict types
- Supports modern features (arrow functions, let, const)
- Offers extra features (generics, interfaces, tuples, etc.)
bashnpm install -g typescript
bashnpm install -g typescript
Compile with different names for TypeScript and JavaScript files:
bashtsc filename.ts filename2.js
bashtsc filename.ts filename2.js
Compile with the same name for TypeScript and JavaScript files:
bashtsc filename.ts
bashtsc filename.ts
Watch for file changes and dynamically recompile:
bashtsc filename.ts -w
bashtsc filename.ts -w
In TypeScript, once a variable's type is defined, it cannot be changed.
ts// No need to specify types; TypeScript infers the type based on the assigned value. let name = 'yooumuu'; // String let age = 30; // Number let isCool = false; // Boolean // Strings can use double or single quotes, just like in JavaScript. let character = 'mario'; // character = 30; // Error, strings can only be reassigned to strings, the same applies to other data types. character = 'luigi'; // ✅
ts// No need to specify types; TypeScript infers the type based on the assigned value. let name = 'yooumuu'; // String let age = 30; // Number let isCool = false; // Boolean // Strings can use double or single quotes, just like in JavaScript. let character = 'mario'; // character = 30; // Error, strings can only be reassigned to strings, the same applies to other data types. character = 'luigi'; // ✅
TypeScript checks types during compilation, preventing type errors before execution.
tsconst circ = (diameter: number) => { return diameter * Math.PI; }; // console.log(circ("hello")); // Error, passing a non-number argument is detected at compile time console.log(circ(7.5)); // ✅
tsconst circ = (diameter: number) => { return diameter * Math.PI; }; // console.log(circ("hello")); // Error, passing a non-number argument is detected at compile time console.log(circ(7.5)); // ✅
tslet names = ['luigi', 'mario', 'yoshi']; // names = "hello"; // Error, variable type cannot change // Once a type is assigned to an array, it cannot be changed. // If the initial array contains strings, only strings can be added later. The same rule applies to numbers and booleans. names.push('toad'); // name.push(3); // Error // name[0] = 3; // Error // names = [0, 1] // Error // Mixed arrays can include types already present in the array. let mixed = ['ken', 4, 'chun-li', 8, 9]; mixed.push('ryu'); mixed.push(10); mixed[0] = 3; // Changing a string to a number is allowed // mixed.push(false); // Error
tslet names = ['luigi', 'mario', 'yoshi']; // names = "hello"; // Error, variable type cannot change // Once a type is assigned to an array, it cannot be changed. // If the initial array contains strings, only strings can be added later. The same rule applies to numbers and booleans. names.push('toad'); // name.push(3); // Error // name[0] = 3; // Error // names = [0, 1] // Error // Mixed arrays can include types already present in the array. let mixed = ['ken', 4, 'chun-li', 8, 9]; mixed.push('ryu'); mixed.push(10); mixed[0] = 3; // Changing a string to a number is allowed // mixed.push(false); // Error
tslet character = { name: 'mario', color: 'red', age: 30, }; // Similarly, object property types cannot be changed once assigned. character.age = 40; character.name = 'ryu'; // character.age = "30"; // Error // You cannot add properties that were not initially defined. // character.skills = ["fighting", "sneaking"]; // Error // When reassigning an object, it must have the same structure, property names, and the same number of properties. character = { name: 'yoshi', color: 'green', age: 34, };
tslet character = { name: 'mario', color: 'red', age: 30, }; // Similarly, object property types cannot be changed once assigned. character.age = 40; character.name = 'ryu'; // character.age = "30"; // Error // You cannot add properties that were not initially defined. // character.skills = ["fighting", "sneaking"]; // Error // When reassigning an object, it must have the same structure, property names, and the same number of properties. character = { name: 'yoshi', color: 'green', age: 34, };
tslet character: string; let age: number; let isLoggedIn: boolean; // age = "luigi"; // Error isLoggedIn = false; // Arrays let characters: string[] = []; // Specify an array containing strings and initialize it as an empty array. characters.push('shaun'); // characters = [0, 1]; // Error characters = ['mario', 'yoshi'];
tslet character: string; let age: number; let isLoggedIn: boolean; // age = "luigi"; // Error isLoggedIn = false; // Arrays let characters: string[] = []; // Specify an array containing strings and initialize it as an empty array. characters.push('shaun'); // characters = [0, 1]; // Error characters = ['mario', 'yoshi'];
tslet uid: string | number; uid = '123'; uid = 123; // uid = false; // Error // Arrays let mixed: (string | number)[] = []; mixed.push('hello'); mixed.push(12); // Objects let characterOne: object; characterOne = { name: 'yoshi', age: 30 }; characterOne = []; // Arrays are a special kind of object // characterOne = " "; // Error // Explicitly specify that a variable is an object and specify the types of its properties: // let characterTwo: {}; let characterTwo: { name: string; age: number; color: string; }; characterTwo = { name: 'mario', age: 30, color: 'red', };
tslet uid: string | number; uid = '123'; uid = 123; // uid = false; // Error // Arrays let mixed: (string | number)[] = []; mixed.push('hello'); mixed.push(12); // Objects let characterOne: object; characterOne = { name: 'yoshi', age: 30 }; characterOne = []; // Arrays are a special kind of object // characterOne = " "; // Error // Explicitly specify that a variable is an object and specify the types of its properties: // let characterTwo: {}; let characterTwo: { name: string; age: number; color: string; }; characterTwo = { name: 'mario', age: 30, color: 'red', };
Note: Be cautious when using any
types.
tslet age: any = 25; any = true; any = 'hello'; any = { name: 'luigi' }; let mixed: any[] = []; mixed.push(5); mixed.push('mario'); mixed.push(false); let character: { name: any; age: any; }; character = { name: 'yoshi', age: 25 }; character = { name: 25, age: 'yoshi' };
tslet age: any = 25; any = true; any = 'hello'; any = { name: 'luigi' }; let mixed: any[] = []; mixed.push(5); mixed.push('mario'); mixed.push(false); let character: { name: any; age: any; }; character = { name: 'yoshi', age: 25 }; character = { name: 25, age: 'yoshi' };
ts// Automatically inferred as a function type. let greet = () => { console.log('hello, world'); }; // greet = "hello"; // Error let greet2: Function; // Specify the variable type as a function with a capital 'F'. greet2 = () => { console.log('hello, again'); }; // Define optional parameters const add = (a: number, b: number, c?: number | string) => { console.log(a + b); console.log(c); // undefined }; // Define default parameters const minus = (a: number, b: number, c: number | string = 10) => { console.log(a - b); console.log(c); // 10 }; // Return type is automatically inferred; if no return, it's inferred as 'void.' const minus2 = (a: number, b: number, c: number | string = 10): number => { return a - b; }; let result = minus2(10, 7); // TypeScript infers 'result' as a number // result = 'something else'; // Error
ts// Automatically inferred as a function type. let greet = () => { console.log('hello, world'); }; // greet = "hello"; // Error let greet2: Function; // Specify the variable type as a function with a capital 'F'. greet2 = () => { console.log('hello, again'); }; // Define optional parameters const add = (a: number, b: number, c?: number | string) => { console.log(a + b); console.log(c); // undefined }; // Define default parameters const minus = (a: number, b: number, c: number | string = 10) => { console.log(a - b); console.log(c); // 10 }; // Return type is automatically inferred; if no return, it's inferred as 'void.' const minus2 = (a: number, b: number, c: number | string = 10): number => { return a - b; }; let result = minus2(10, 7); // TypeScript infers 'result' as a number // result = 'something else'; // Error
ts// Before: const logDetails = (uid: string | number, item: string) => { console.log(`${item} has a uid of ${uid}`); }; const greet = (user: { name: string; uid: string | number }) => { console.log(`${user.name} says hello`); }; // After: type StringOrNum = string | number; type objWithName = { name: string; uid: StringOrNum }; const logDetails = (uid: StringOrNum, item: string) => { console.log(`${item} has a uid of ${uid}`); }; const greet = (user: objWithName) => { console.log(`${user.name} says hello`); };
ts// Before: const logDetails = (uid: string | number, item: string) => { console.log(`${item} has a uid of ${uid}`); }; const greet = (user: { name: string; uid: string | number }) => { console.log(`${user.name} says hello`); }; // After: type StringOrNum = string | number; type objWithName = { name: string; uid: StringOrNum }; const logDetails = (uid: StringOrNum, item: string) => { console.log(`${item} has a uid of ${uid}`); }; const greet = (user: objWithName) => { console.log(`${user.name} says hello`); };
ts// Example 1 let greet: (a: string, b: string) => void; // 'greet' is a function that takes two string arguments and returns 'void.' greet = (name: string, greeting: string) => { console.log(`${name} says ${greeting}`); }; // Example 2 let calc: (a: number, b: number, c: string) => number; // 'calc' is a function that takes two number arguments and a string argument and returns a number. calc = (numOne: number, numTwo: number, action: string) => { if (action === 'add') { return numOne + numTwo; } else { // Since the return type is number, there must be an 'else' statement. return numOne - numTwo; } }; // Example 3 let logDetails: (obj: { name: string; age: number }) => void; // 'logDetails' is a function that takes an object with 'name' and 'age' properties and returns 'void.' // Combined with type aliases type person = { name: string; age: number }; logDetails = (character: person) => { console.log(`${character.name} is ${character.age} years old`); };
ts// Example 1 let greet: (a: string, b: string) => void; // 'greet' is a function that takes two string arguments and returns 'void.' greet = (name: string, greeting: string) => { console.log(`${name} says ${greeting}`); }; // Example 2 let calc: (a: number, b: number, c: string) => number; // 'calc' is a function that takes two number arguments and a string argument and returns a number. calc = (numOne: number, numTwo: number, action: string) => { if (action === 'add') { return numOne + numTwo; } else { // Since the return type is number, there must be an 'else' statement. return numOne - numTwo; } }; // Example 3 let logDetails: (obj: { name: string; age: number }) => void; // 'logDetails' is a function that takes an object with 'name' and 'age' properties and returns 'void.' // Combined with type aliases type person = { name: string; age: number }; logDetails = (character: person) => { console.log(`${character.name} is ${character.age} years old`); };
tsconst anchor = document.querySelector('a'); console.log(anchor); // <a href="https://www.google.com">Google</a> // console.log(anchor.href); // Error, TypeScript doesn't know the type of this element, so you can't directly use the 'href' property. // Use type casting to inform TypeScript about the element's type. // 1. Use an if/else statement. if (anchor) { console.log(anchor.href); } // 2. Use '!' after variable assignment to assert that it is not null. const anchor2 = document.querySelector('a')!; console.log(anchor2.href); // ✅ // TypeScript recognizes this variable as an HTMLAnchorElement, allowing auto-completion and type-specific methods/properties. // const form = document.querySelector('form')!; const form = document.querySelector('.new-item-form') as HTMLFormElement; // When using class, id, tag name selectors, TypeScript automatically infers them as HTMLElements, so you need to manually specify the type using 'as HTMLFormElement'. You don't need to add '!' because TypeScript knows the variable is not null. console.log(form.children); // HTMLCollection [input#type, input#tofrom, input#details, button, button] // Inputs const type = document.querySelector('#type') as HTMLSelectElement; const tofrom = document.querySelector('#tofrom') as HTMLInputElement; const details = document.querySelector('#details') as HTMLInputElement; const amount = document.querySelector('#amount') as HTMLInputElement; form.addEventListener('submit', (e: Event) => { e.preventDefault(); console.log(type.value, tofrom.value, details.value, amount.valueAsNumber); }); // Use 'valueAsNumber' to directly retrieve numeric input values instead of strings.
tsconst anchor = document.querySelector('a'); console.log(anchor); // <a href="https://www.google.com">Google</a> // console.log(anchor.href); // Error, TypeScript doesn't know the type of this element, so you can't directly use the 'href' property. // Use type casting to inform TypeScript about the element's type. // 1. Use an if/else statement. if (anchor) { console.log(anchor.href); } // 2. Use '!' after variable assignment to assert that it is not null. const anchor2 = document.querySelector('a')!; console.log(anchor2.href); // ✅ // TypeScript recognizes this variable as an HTMLAnchorElement, allowing auto-completion and type-specific methods/properties. // const form = document.querySelector('form')!; const form = document.querySelector('.new-item-form') as HTMLFormElement; // When using class, id, tag name selectors, TypeScript automatically infers them as HTMLElements, so you need to manually specify the type using 'as HTMLFormElement'. You don't need to add '!' because TypeScript knows the variable is not null. console.log(form.children); // HTMLCollection [input#type, input#tofrom, input#details, button, button] // Inputs const type = document.querySelector('#type') as HTMLSelectElement; const tofrom = document.querySelector('#tofrom') as HTMLInputElement; const details = document.querySelector('#details') as HTMLInputElement; const amount = document.querySelector('#amount') as HTMLInputElement; form.addEventListener('submit', (e: Event) => { e.preventDefault(); console.log(type.value, tofrom.value, details.value, amount.valueAsNumber); }); // Use 'valueAsNumber' to directly retrieve numeric input values instead of strings.
tsclass Invoice { // Define properties of this class client: string; details: string; amount: number; // Initialize properties in the constructor constructor(c: string, d: string, a: number) { this.client = c; this.details = d; this.amount = a; } // Define methods of this class format() { return `${this.client} owes $${this.amount} for ${this.details}`; } } // Instantiate objects of this class const invOne = new Invoice('mario', 'work on the mario website', 250); const invTwo = new Invoice('luigi', 'work on the luigi website', 300); let invoices: Invoice[] = []; // Specify that this array contains Invoice objects // Default class property access is 'public'; you can access and modify it outside the class. invOne.client = 'yoshi'; invTwo.amount = 400;
tsclass Invoice { // Define properties of this class client: string; details: string; amount: number; // Initialize properties in the constructor constructor(c: string, d: string, a: number) { this.client = c; this.details = d; this.amount = a; } // Define methods of this class format() { return `${this.client} owes $${this.amount} for ${this.details}`; } } // Instantiate objects of this class const invOne = new Invoice('mario', 'work on the mario website', 250); const invTwo = new Invoice('luigi', 'work on the luigi website', 300); let invoices: Invoice[] = []; // Specify that this array contains Invoice objects // Default class property access is 'public'; you can access and modify it outside the class. invOne.client = 'yoshi'; invTwo.amount = 400;
By default, class properties are public, meaning they can be accessed and modified from outside the class. You can change this behavior using private
, public
, and readonly
.
tsclass Invoice { // Define properties of this class readonly client: string; // Readonly property, cannot be modified private details: string; // Private property, can only be accessed within the class public amount: number; // Public property, can be accessed and modified outside the class // Initialize properties in the constructor constructor(c: string, d: string, a: number) { this.client = c; this.details = d; this.amount = a; } } // Shorter syntax class Invoice { constructor( public client: string, private details: string, public amount: number ) {} }
tsclass Invoice { // Define properties of this class readonly client: string; // Readonly property, cannot be modified private details: string; // Private property, can only be accessed within the class public amount: number; // Public property, can be accessed and modified outside the class // Initialize properties in the constructor constructor(c: string, d: string, a: number) { this.client = c; this.details = d; this.amount = a; } } // Shorter syntax class Invoice { constructor( public client: string, private details: string, public amount: number ) {} }
To use import
and export
, set "module": "es2015"
or "module": "ES6"
in tsconfig.json
and add type="module"
to your script tags:
html<script type="module" src="app.js"></script>
html<script type="module" src="app.js"></script>
Interfaces are used to define the structure of objects, including properties and methods. Key characteristics:
- An interface is a type, just like other TypeScript types (e.g., string, number).
- Interfaces can describe the shape of an object, including multiple properties and methods.
- A class can implement an interface, requiring it to implement all of its properties and methods.
- Interfaces can extend other interfaces,
allowing the creation of more specific interfaces.
tsinterface IsPerson { name: string; age: number; speak(a: string): void; spend(a: number): number; } const me: IsPerson = { name: 'shaun', age: 30, speak(text: string): void { console.log(text); }, spend(amount: number): number { console.log('I spent', amount); return amount; }, }; let someone: IsPerson; // The type of 'someone' is IsPerson. const greetPerson = (person: IsPerson) => { console.log('hello', person.name); };
tsinterface IsPerson { name: string; age: number; speak(a: string): void; spend(a: number): number; } const me: IsPerson = { name: 'shaun', age: 30, speak(text: string): void { console.log(text); }, spend(amount: number): number { console.log('I spent', amount); return amount; }, }; let someone: IsPerson; // The type of 'someone' is IsPerson. const greetPerson = (person: IsPerson) => { console.log('hello', person.name); };
You can implement interfaces in classes to ensure they have the required structure:
tsimport { HasFormatter } from '../interfaces/HasFormatter.js'; export class Invoice implements HasFormatter { constructor( readonly client: string, private details: string, public amount: number ) {} format() { return `${this.client} owes $${this.amount} for ${this.details}`; } }
tsimport { HasFormatter } from '../interfaces/HasFormatter.js'; export class Invoice implements HasFormatter { constructor( readonly client: string, private details: string, public amount: number ) {} format() { return `${this.client} owes $${this.amount} for ${this.details}`; } }
Generics allow you to write code that works with various data types while preserving type safety. Key points:
- Generics can be used with functions, classes, interfaces, and type aliases.
- You can specify type parameters when defining generics and provide specific types when using them.
- Constraints can be applied to type parameters to restrict the allowed types.
- Default types can be specified for type parameters.
tsconst addUID = <T>(obj: T) => { let uid = Math.floor(Math.random() * 100); return { ...obj, uid }; }; let docOne = addUID({ name: 'yoshi', age: 40 }); console.log(docOne); const addUID = <T extends object>(obj: T) => { let uid = Math.floor(Math.random() * 100); return { ...obj, uid }; }; const addUID = <T extends { name: string }>(obj: T) => { let uid = Math.floor(Math.random() * 100); return { ...obj, uid }; };
tsconst addUID = <T>(obj: T) => { let uid = Math.floor(Math.random() * 100); return { ...obj, uid }; }; let docOne = addUID({ name: 'yoshi', age: 40 }); console.log(docOne); const addUID = <T extends object>(obj: T) => { let uid = Math.floor(Math.random() * 100); return { ...obj, uid }; }; const addUID = <T extends { name: string }>(obj: T) => { let uid = Math.floor(Math.random() * 100); return { ...obj, uid }; };
Enums allow you to define a set of named numeric values. They can be used to represent a set of related constants. Key features:
- Enums provide named values for a set of related constants.
- Enums can be used to assign human-readable names to numeric values.
- Enums can be used to represent a set of related options or choices.
tsenum ResourceType { BOOK, AUTHOR, FILM, DIRECTOR, PERSON, } interface Resource<T> { uid: number; resourceType: ResourceType; data: T; } const docOne: Resource<object> = { uid: 1, resourceType: ResourceType.BOOK, data: { title: 'name of the wind' }, }; const docTwo: Resource<object> = { uid: 10, resourceType: ResourceType.PERSON, data: { name: 'yoshi' }, };
tsenum ResourceType { BOOK, AUTHOR, FILM, DIRECTOR, PERSON, } interface Resource<T> { uid: number; resourceType: ResourceType; data: T; } const docOne: Resource<object> = { uid: 1, resourceType: ResourceType.BOOK, data: { title: 'name of the wind' }, }; const docTwo: Resource<object> = { uid: 10, resourceType: ResourceType.PERSON, data: { name: 'yoshi' }, };
Tuples are a specialized array type that allows you to specify the type and order of elements. Key points:
- Tuples have a fixed length and a specific order of elements.
- Each element in a tuple can have a different type.
- Accessing elements in a tuple is based on their position.
tslet tup: [string, number, boolean] = ['ryu', 25, true]; tup[0] = 'ken'; // Values can be reassigned, but types cannot be changed. // tup[0] = 30; // Error let student: [string, number]; student = ['chun-li', 223423]; // student = [223423, 'chun-li']; // Error let values: [string, string, number]; values = [tofrom.value, details.value, amount.valueAsNumber]; if (type.value === 'invoice') { doc = new Invoice(...values); } else { doc = new Payment(...values); }
tslet tup: [string, number, boolean] = ['ryu', 25, true]; tup[0] = 'ken'; // Values can be reassigned, but types cannot be changed. // tup[0] = 30; // Error let student: [string, number]; student = ['chun-li', 223423]; // student = [223423, 'chun-li']; // Error let values: [string, string, number]; values = [tofrom.value, details.value, amount.valueAsNumber]; if (type.value === 'invoice') { doc = new Invoice(...values); } else { doc = new Payment(...values); }
ts// Using an interface to define an object's structure interface Car { brand: string; model: string; year: number; } // Using a type alias to define the same object structure type CarType = { brand: string; model: string; year: number; }; // Creating an object that conforms to the interface const myCar: Car = { brand: 'Toyota', model: 'Camry', year: 2022, }; // Creating an object that conforms to the type alias const myCarType: CarType = { brand: 'Honda', model: 'Civic', year: 2023, }; // Both interfaces and type aliases support optional properties interface Person { name: string; age?: number; } type PersonType = { name: string; age?: number; }; // Implementing an interface requires adhering to its structure class Student implements Person { constructor(public name: string, public age: number) {} } // Implementing a type alias also requires adhering to its structure class Teacher implements PersonType { constructor(public name: string, public age: number) {} }
ts// Using an interface to define an object's structure interface Car { brand: string; model: string; year: number; } // Using a type alias to define the same object structure type CarType = { brand: string; model: string; year: number; }; // Creating an object that conforms to the interface const myCar: Car = { brand: 'Toyota', model: 'Camry', year: 2022, }; // Creating an object that conforms to the type alias const myCarType: CarType = { brand: 'Honda', model: 'Civic', year: 2023, }; // Both interfaces and type aliases support optional properties interface Person { name: string; age?: number; } type PersonType = { name: string; age?: number; }; // Implementing an interface requires adhering to its structure class Student implements Person { constructor(public name: string, public age: number) {} } // Implementing a type alias also requires adhering to its structure class Teacher implements PersonType { constructor(public name: string, public age: number) {} }