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
- 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.
-
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 atsconfig.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, whileesnext
is used for modern browser environments.strict
: Highly recommended. Enables a set of strict type-checking rules, catching more potential errors. This includesnoImplicitAny
,strictNullChecks
, and more.outDir
: Where your compiled JavaScript files will be placed.rootDir
: The root directory of your TypeScript source files.include
andexclude
: 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
: Representstrue
orfalse
values.typescript
let isLoggedIn: boolean = true;
let isComplete: boolean = false; -
null
andundefined
: Represent the absence of a value.null
typically represents an intentional absence, whileundefined
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 toany
, but safer. You must perform type checking or type assertion before using anunknown
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() {
returnHello, 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
return arg;
}
let myString: string = identity
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
const numberHolder = new DataHolder(123);
//Generic Constraints
interface Lengthwise {
length: number;
}
function loggingIdentity
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 = (
“`
* 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 ofT
optional.“`typescript
interface Todo {
title: string;
description: string;
}type PartialTodo = Partial
; // { title?: string; description?: string; }
“` -
Required<T>
: Makes all properties ofT
required.“`typescript
interface OptionalProps {
a?: number;
b?: string;
}type RequiredProps = Required
; // { a: number; b: string; }
“` -
Readonly<T>
: Makes all properties ofT
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
) fromT
.“`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
) fromT
.typescript
type UserWithoutEmail = Omit<User, "email">; // { id: number; name: string; } -
Record<K, T>
: Creates a type with keys of typeK
and values of typeT
.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 fromT
all properties that are assignable toU
.typescript
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c" -
Extract<T, U>
: Constructs a type by extracting fromT
all properties that are assignable toU
.typescript
type T1 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
*NonNullable<T>
: Constructs a type by excludingnull
andundefined
fromT
.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 typeT
.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 functionT
.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
intsconfig.json
: Enforce the strictest type checking. - Avoid
any
whenever possible: Strive for strong typing. Useunknown
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!