The Only TypeScript Tutorial You’ll Ever Need

The Only TypeScript Tutorial You’ll Ever Need

Okay, let’s be honest. There’s no single tutorial that covers everything in TypeScript. The language is constantly evolving. However, this guide aims to be the most comprehensive, practical, and approachable introduction you’ll find, laying a solid foundation for becoming a proficient TypeScript developer. We’ll go beyond the basics and delve into more advanced concepts, all with clear explanations and practical examples.

1. Why TypeScript? The Case for Strong Typing

JavaScript, while incredibly flexible, is dynamically typed. This means type checking happens at runtime. This can lead to unexpected errors that only surface when the code is running, often in production! TypeScript, on the other hand, is a statically typed superset of JavaScript. This means:

  • Type Checking at Compile Time: Errors related to incorrect types are caught before your code runs, significantly reducing bugs and improving code reliability.
  • Improved Code Maintainability: Type annotations make your code more readable and understandable, especially in larger projects. It’s self-documenting.
  • Better Developer Experience: TypeScript provides excellent autocompletion, refactoring support, and error highlighting in your IDE (like VS Code), making development faster and more efficient.
  • Gradual Adoption: You can introduce TypeScript incrementally into existing JavaScript projects.

2. Setting Up Your TypeScript Environment

  1. Install Node.js and npm: TypeScript relies on Node.js and its package manager, npm. Download and install the latest LTS version from nodejs.org.
  2. Install TypeScript Globally: Open your terminal or command prompt and run:

    bash
    npm install -g typescript

    3. Verify Installation: Check the installed version:

    bash
    tsc -v

    4. Create tsconfig.json: This file contains all your compiler options. Run the following in your project’s root folder:

    bash
    tsc --init

    This creates a tsconfig.json with sensible defaults. Let’s look at some key options:

    json
    {
    "compilerOptions": {
    "target": "ES2016", // Specifies the ECMAScript target version (ES5, ES6, ES2015, etc.)
    "module": "commonjs", // Specifies the module system (commonjs, esnext, etc.)
    "strict": true, // Enables a wide range of type checking rules for stricter code
    "esModuleInterop": true, // Allows default imports from modules with no default export (for CommonJS compatibility)
    "skipLibCheck": true, // Skip type checking of declaration files (*.d.ts)
    "forceConsistentCasingInFileNames": true, // Disallows inconsistent casing in file names
    "outDir": "./dist", // Specifies the output directory for compiled JavaScript files
    "sourceMap": true, // Generates source map files for easier debugging
    "rootDir": "./src" // Specifies the root directory of your TypeScript source files
    },
    "include": ["src/**/*"], // Specifies which files to include in compilation
    "exclude": ["node_modules"] // Specifies which files to exclude from compilation
    }

    • target: Determines the JavaScript version your TypeScript code will be compiled to. ES2016 is a good modern default.
    • module: Specifies how modules are handled. commonjs is common for Node.js, while esnext is used for modern browser environments.
    • strict: Highly recommended. Enables a set of strict type-checking rules, catching more potential errors. This includes noImplicitAny, strictNullChecks, and more.
    • outDir: Where your compiled JavaScript files will be placed.
    • rootDir: The root directory of your TypeScript source files.
    • include and exclude: Control which files are included and excluded from compilation.

3. Basic Types: The Building Blocks

TypeScript introduces several basic types:

  • number: Represents all numbers (integers and floating-point).

    typescript
    let age: number = 30;
    let price: number = 99.99;

  • string: Represents textual data.

    typescript
    let name: string = "Alice";
    let message: string = `Hello, ${name}!`; // Template literals

  • boolean: Represents true or false values.

    typescript
    let isLoggedIn: boolean = true;
    let isComplete: boolean = false;

  • null and undefined: Represent the absence of a value. null typically represents an intentional absence, while undefined indicates a variable that has been declared but not assigned a value.

    typescript
    let data: null = null;
    let something: undefined = undefined;

  • any: Use sparingly! any essentially disables type checking for a variable. It’s useful when you’re working with dynamic data or migrating from JavaScript, but should be avoided whenever possible.

    typescript
    let unknownValue: any = "could be anything";

  • void: Represents the absence of a return value from a function.

    typescript
    function logMessage(message: string): void {
    console.log(message);
    }

  • never: Represents a type that never occurs. Used for functions that never return (e.g., throw an error or have an infinite loop).

    “`typescript
    function throwError(message: string): never {
    throw new Error(message);
    }

    function infiniteLoop(): never {
    while (true) {}
    }
    “`

  • unknown: Similar to any, but safer. You must perform type checking or type assertion before using an unknown value.

    “`typescript
    let userInput: unknown;
    userInput = “Hello”;

    if (typeof userInput === “string”) {
    console.log(userInput.toUpperCase()); // Safe because we checked the type
    }
    “`

  • Arrays:
    “`typescript
    let numbers: number[] = [1, 2, 3];
    let names: string[] = [“Alice”, “Bob”, “Charlie”];
    let mixed: (number | string)[] = [1, “two”, 3]; // Array of numbers or strings

    //Alternative syntax with Array
    let list: Array = [1, 2, 3];
    “`
    * Tuples: Fixed-length arrays where the type of each element is known.

    typescript
    let person: [string, number] = ["Alice", 30];
    // person = [30, "Alice"]; // Error: Type 'number' is not assignable to type 'string'.

4. Functions: Defining Input and Output Types

“`typescript
// Function with explicit parameter and return types
function add(x: number, y: number): number {
return x + y;
}

// Optional parameters (using ?)
function greet(name: string, greeting?: string): string {
if (greeting) {
return ${greeting}, ${name}!;
}
return Hello, ${name}!;
}

// Default parameter values
function multiply(a: number, b: number = 2): number {
return a * b;
}

// Rest parameters (collects remaining arguments into an array)
function sum(…numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}

// Function types (describing the type of a function)
let myAdd: (x: number, y: number) => number = add;

//Function overloads: specify multiple function signatures for different argument types
function processInput(input: string): string;
function processInput(input: number): number;
function processInput(input: any): any {
if (typeof input === ‘string’) {
return input.toUpperCase();
} else if (typeof input === ‘number’) {
return input * 2;
}
}
“`

5. Objects and Interfaces: Defining Shapes

  • Objects:

    typescript
    let user: { name: string; age: number } = {
    name: "Bob",
    age: 25,
    };

  • Interfaces: Define contracts for object shapes. They are the preferred way to describe object types.

    “`typescript
    interface User {
    name: string;
    age: number;
    email?: string; // Optional property
    greet(): string; // Method signature
    }

    let user1: User = {
    name: “Alice”,
    age: 30,
    greet() {
    return Hello, my name is ${this.name};
    },
    };

    // Extending interfaces
    interface AdminUser extends User {
    role: string;
    }

    let admin: AdminUser = {
    name: “Eve”,
    age: 45,
    email: “[email protected]”,
    role: “administrator”,
    greet: () => {return “Hi, I am ” + admin.name}
    }
    “`

6. Classes: Object-Oriented Programming

“`typescript
class Animal {
name: string; // Property

constructor(name: string) { // Constructor
this.name = name;
}

move(distance: number = 0): void { // Method
console.log(${this.name} moved ${distance}m.);
}
}

class Dog extends Animal { // Inheritance
bark(): void {
console.log(“Woof!”);
}
}

const dog = new Dog(“Buddy”);
dog.move(10); // Output: Buddy moved 10m.
dog.bark(); // Output: Woof!

//Access Modifiers
class Person {
public name: string; // Accessible from anywhere
private age: number; // Accessible only within the Person class
protected id: number; // Accessible within the Person class and its subclasses

constructor(name: string, age: number, id:number) {
    this.name = name;
    this.age = age;
    this.id = id;
}

public introduce(): string {
    return `Hi, I'm ${this.name}, and I'm ${this.age} years old.`;
}

}

class Student extends Person{
studentId: number;
constructor(name: string, age: number, id: number, studentId: number){
super(name, age, id);
this.studentId = studentId;
}

getStudentInfo(): string {
  return `Student ID: ${this.studentId} Name: ${this.name} ID: ${this.id}`;
}

}

const person = new Person(“Alice”, 30, 1);
console.log(person.name); // OK
// console.log(person.age); // Error: Property ‘age’ is private and only accessible within class ‘Person’.

const student = new Student(“Bob”, 24, 2, 11234)
console.log(student.getStudentInfo())

//Abstract Class
abstract class Shape {
abstract getArea(): number; // Abstract method, must be implemented by subclasses

displayType(): void {
    console.log("This is a shape.");
}

}

class Circle extends Shape {
radius: number;

constructor(radius: number) {
    super();
    this.radius = radius;
}

getArea(): number {
    return Math.PI * this.radius * this.radius;
}

}

const circle = new Circle(5);
console.log(circle.getArea()); // 78.53981633974483
circle.displayType(); // This is a shape.
``
* **
abstract` classes:** Cannot be instantiated directly. They serve as blueprints for subclasses.

7. Type Aliases: Naming Complex Types

“`typescript
type Point = {
x: number;
y: number;
};

type StringOrNumber = string | number; // Union type

let origin: Point = { x: 0, y: 0 };
let value: StringOrNumber = “hello”;
value = 123;
“`
Type aliases are similar to interfaces, but they can also be used to give names to primitive types, unions, tuples, and intersections. Use interfaces when you need to define the shape of an object or class, and use type aliases for other types. In general, prefer interfaces over type aliases for object shapes because interfaces support declaration merging.

8. Union and Intersection Types: Combining Types

  • Union Types (|): A variable can be one of several types.

    typescript
    function printId(id: number | string) {
    if (typeof id === "string") {
    console.log(id.toUpperCase());
    } else {
    console.log(id);
    }
    }

  • Intersection Types (&): Combines multiple types into one.

    “`typescript
    interface Loggable {
    log(): void;
    }

    interface Serializable {
    serialize(): string;
    }

    type MyObject = Loggable & Serializable;

    const obj: MyObject = {
    log() {
    console.log(“Logging…”);
    },
    serialize() {
    return “Serialized data”;
    },
    };
    “`

9. Generics: Reusable Type-Safe Components

Generics allow you to write functions and classes that work with multiple types without sacrificing type safety.

“`typescript
function identity(arg: T): T {
return arg;
}

let myString: string = identity(“hello”); // Explicitly specify the type
let myNumber: number = identity(42); // Type inference (TypeScript infers the type)

// Generic class
class DataHolder {
data: T;

constructor(data: T) {
this.data = data;
}

getData(): T {
return this.data;
}
}

const stringHolder = new DataHolder(“TypeScript”);
const numberHolder = new DataHolder(123);

//Generic Constraints
interface Lengthwise {
length: number;
}

function loggingIdentity(arg: T): T {
console.log(arg.length); // Now we know it has a .length property
return arg;
}
//Generic in Interface
interface GenericIdentityFn {
(arg: T): T;
}
``
* **
T` (or any other letter) is a type parameter. It’s a placeholder for a specific type that will be provided later.

10. Type Assertions: Telling the Compiler What You Know

Sometimes, you know more about the type of a value than TypeScript does. Type assertions let you tell the compiler the type.

“`typescript
let someValue: unknown = “this is a string”;

// Using the “as” syntax
let strLength: number = (someValue as string).length;

// Using the angle-bracket syntax (not recommended in .tsx files)
let strLength2: number = (someValue).length;
“`
* Important: Type assertions are a compile-time construct. They don’t perform any runtime checks. If you’re wrong, you can still get runtime errors. Use them carefully!

11. Type Guards: Narrowing Types at Runtime

Type guards are functions that return a boolean, indicating whether a value is of a specific type. They help you narrow down union types.

“`typescript
function isString(value: any): value is string { // “value is string” is the type predicate
return typeof value === “string”;
}

function processValue(value: string | number) {
if (isString(value)) {
// Inside this block, TypeScript knows ‘value’ is a string
console.log(value.toUpperCase());
} else {
// Here, ‘value’ must be a number
console.log(value.toFixed(2));
}
}

//Using instanceof
class Cat {
meow() {
console.log(‘meow’)
}
}
class Bird {
chirp() {
console.log(‘chirp’)
}
}

function petSound(pet: Cat | Bird) {
if(pet instanceof Cat) {
pet.meow()
}
if(pet instanceof Bird) {
pet.chirp()
}
}
“`

12. Literal Types: Specific Values as Types

“`typescript
type Direction = “north” | “south” | “east” | “west”;

let myDirection: Direction = “north”;
// myDirection = “up”; // Error: Type ‘”up”‘ is not assignable to type ‘Direction’.

//Numeric Literal Types
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 4;

//String Literal Types in combination with Union Types
function setAlignment(alignment: “left” | “right” | “center”) {
// …
}
“`

13. Enums: Named Constants

“`typescript
enum Color {
Red, // 0
Green, // 1
Blue, // 2
}

let myColor: Color = Color.Green;
console.log(myColor); // Output: 1

//Custom values
enum StatusCode {
Success = 200,
NotFound = 404,
ServerError = 500,
}
//String enums
enum LogLevel {
ERROR = ‘error’,
WARN = ‘warn’,
INFO = ‘info’,
DEBUG = ‘debug’,
}
“`
Enums are a way to give more friendly names to sets of numeric (or string) values.

14. Utility Types: Transforming Existing Types

TypeScript provides several utility types that help you transform and manipulate existing types.

  • Partial<T>: Makes all properties of T optional.

    “`typescript
    interface Todo {
    title: string;
    description: string;
    }

    type PartialTodo = Partial; // { title?: string; description?: string; }
    “`

  • Required<T>: Makes all properties of T required.

    “`typescript
    interface OptionalProps {
    a?: number;
    b?: string;
    }

    type RequiredProps = Required; // { a: number; b: string; }
    “`

  • Readonly<T>: Makes all properties of T readonly.

    “`typescript
    interface Point {
    x: number;
    y: number;
    }

    type ReadonlyPoint = Readonly; // { readonly x: number; readonly y: number; }
    “`

  • Pick<T, K>: Creates a new type by picking specific properties (K) from T.

    “`typescript
    interface User {
    id: number;
    name: string;
    email: string;
    }

    type UserNameAndId = Pick; // { name: string; id: number; }
    “`

  • Omit<T, K>: Creates a new type by omitting specific properties (K) from T.

    typescript
    type UserWithoutEmail = Omit<User, "email">; // { id: number; name: string; }

  • Record<K, T>: Creates a type with keys of type K and values of type T.

    typescript
    type PageInfo = Record<string, number>; // { [key: string]: number; }
    const pageCounts: PageInfo = {
    home: 10,
    about: 5,
    contact: 20
    };

  • Exclude<T, U>: Constructs a type by excluding from T all properties that are assignable to U.

    typescript
    type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"

  • Extract<T, U>: Constructs a type by extracting from T all properties that are assignable to U.

    typescript
    type T1 = Extract<"a" | "b" | "c", "a" | "f">; // "a"

    * NonNullable<T>: Constructs a type by excluding null and undefined from T.

    typescript
    type T2 = NonNullable<string | number | undefined | null>; // string | number

  • Parameters<T>: Constructs a tuple type from the types used in the parameters of a function type T.

    typescript
    function f1(arg: { a: number; b: string }): void {}
    type T3 = Parameters<typeof f1>; // [{ a: number; b: string; }]

  • ReturnType<T>: Constructs a type consisting of the return type of function T.

    typescript
    type T4 = ReturnType<typeof f1>; // void

    * InstanceType<T>: Constructs a type consisting of the instance type of a constructor function type T.

typescript
class C {
x = 0;
y = 0;
}
type T5 = InstanceType<typeof C>; // C

15. Declaration Files (.d.ts): Typing External Libraries

When you use a JavaScript library that doesn’t have built-in TypeScript types, you can use declaration files (.d.ts) to provide type information.

  • DefinitelyTyped: The most common way to get types for existing JavaScript libraries is through the DefinitelyTyped project, a community-maintained repository of declaration files.

    bash
    npm install --save-dev @types/lodash # Example: Install types for Lodash

  • Creating Your Own: If types aren’t available, you can create your own .d.ts file to describe the library’s API.

    typescript
    // my-library.d.ts
    declare module "my-library" {
    export function greet(name: string): string;
    export const version: string;
    }

    16. Advanced Concepts

  • Conditional Types: Types that depend on a condition.

    “`typescript
    type IsString = T extends string ? “yes” : “no”;

    type Result1 = IsString; // “yes”
    type Result2 = IsString; // “no”
    “`

  • Mapped Types: Create new types based on existing ones by transforming properties.

    “`typescript
    type OptionsFlags = {

    };

    interface FeatureFlags {
    darkMode: string;
    notifications: number
    }

    type FeatureOptions = OptionsFlags; // { darkMode: boolean; notifications: boolean; }
    “`

  • Infer Keyword: Used within conditional types to infer a type from within another type.
    “`typescript
    type ReturnType = T extends (…args: any[]) => infer R ? R : any;

    function exampleFunction(): number {
    return 42;
    }

    type ExampleReturnType = ReturnType; // number
    “`

  • Keyof Type Operator: The keyof operator takes an object type and produces a string or numeric literal union of its keys.
    “`typescript
    interface Person {
    name: string;
    age: number;
    }

type PersonKeys = keyof Person; // “name” | “age”
* **Typeof Type Operator**:The typeof operator can be used in a type context to refer to the type of a variable or property.typescript
let s = “hello”;
let n: typeof s; // let n: string
* **Indexed Access Types**: We can use an indexed access type to look up a specific property on another type.typescript
type Person = { age: number; name: string; alive: boolean };
type Age = Person[“age”]; // type Age = number
* **Decorators**: Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members.typescript
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}

@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return “Hello, ” + this.greeting;
}
}
“`
17. Best Practices

  • Use strict: true in tsconfig.json: Enforce the strictest type checking.
  • Avoid any whenever possible: Strive for strong typing. Use unknown as a safer alternative when dealing with unknown types.
  • Use interfaces for object shapes: They are more readable and support declaration merging.
  • Use type aliases for complex types: Give names to unions, intersections, tuples, etc.
  • Use generics for reusable components: Write type-safe code that works with multiple types.
  • Use type guards to narrow union types: Improve type safety at runtime.
  • Use declaration files for external libraries: Get type information for JavaScript libraries.
  • Keep your code DRY (Don’t Repeat Yourself): Use generics, utility types, and mapped types to avoid redundant type definitions.
  • Write clear and concise code: Use meaningful variable and function names. Add comments where necessary.
  • Test your code: Write unit tests to ensure your code works as expected, including type checks.
  • Use a Linter: A linter like ESLint with the @typescript-eslint/eslint-plugin can help enforce coding standards and best practices, including TypeScript-specific rules.
  • Use a Formatter: A code formatter like Prettier can automatically format your code, ensuring consistent style across your project.
  • Stay Updated: TypeScript is constantly evolving. Keep up with new features and best practices.

18. Example Project: A Simple Todo List

Let’s tie everything together with a basic Todo List application:

src/types.ts:

typescript
export interface Todo {
id: number;
text: string;
completed: boolean;
}

src/todo.ts:

“`typescript
import { Todo } from “./types”;

class TodoList {
private todos: Todo[] = [];
private nextId: number = 1;

addTodo(text: string): Todo {
const newTodo: Todo = {
id: this.nextId++,
text,
completed: false,
};
this.todos.push(newTodo);
return newTodo;
}

getTodos(): Todo[] {
return this.todos;
}

completeTodo(id: number): void {
const todo = this.todos.find((t) => t.id === id);
if (todo) {
todo.completed = true;
}
}

deleteTodo(id:number):void {
this.todos = this.todos.filter(todo => todo.id !== id)
}
}

const todoList = new TodoList();
const todo1 = todoList.addTodo(“Learn TypeScript”);
const todo2 = todoList.addTodo(“Build a project”);
console.log(todoList.getTodos()); // Output: [{ id: 1, text: ‘Learn TypeScript’, completed: false }, { id: 2, text: ‘Build a project’, completed: false }]
todoList.completeTodo(1);
console.log(todoList.getTodos()); // Output: [{ id: 1, text: ‘Learn TypeScript’, completed: true }, { id: 2, text: ‘Build a project’, completed: false }]
todoList.deleteTodo(1);
console.log(todoList.getTodos());
“`

tsconfig.json: (Use the configuration from Section 2)

To compile, run tsc in your project’s root directory. This will generate the corresponding JavaScript files in the dist directory.

This example demonstrates:

  • Interfaces (Todo)
  • Classes (TodoList)
  • Methods with type annotations
  • Array manipulation
  • Basic logic

This tutorial covers the most essential aspects of TypeScript, from basic types to advanced concepts like generics, utility types, and declaration files. It’s a strong foundation for learning and using TypeScript effectively. Remember to practice, experiment, and explore the official TypeScript documentation (typescriptlang.org) for even deeper understanding. Good luck!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top