Introduction to TypeScript: A Guide for JavaScript Developers
TypeScript has become an essential tool in modern web development, offering type safety and enhanced developer experience on top of JavaScript. If you're a JavaScript developer looking to level up your skills, TypeScript is a natural next step. This guide will introduce you to TypeScript's core concepts and show you how to get started.
What is TypeScript?
TypeScript is a strongly typed programming language that builds on JavaScript. It was developed and is maintained by Microsoft. TypeScript adds optional static typing and other features to JavaScript, making it easier to develop and maintain large-scale applications.
Key points about TypeScript:
- It's a superset of JavaScript (all valid JavaScript is also valid TypeScript)
- It compiles down to plain JavaScript that runs in any browser or JavaScript environment
- It provides static type checking at compile time
- It offers enhanced IDE support with better autocompletion, navigation, and refactoring tools
- It's open source and constantly evolving
Why Use TypeScript?
Before diving into the syntax, let's understand why you might want to use TypeScript:
1. Catch Errors Earlier
With static type checking, many errors that would only be discovered at runtime in JavaScript are caught during development:
// JavaScript
function add(a, b) {
return a + b;
}
add("5", 10); // Returns "510" - probably not what you wanted
// TypeScript
function add(a: number, b: number): number {
return a + b;
}
add("5", 10); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
2. Better Developer Experience
TypeScript provides excellent tooling that enhances the development experience:
- Rich autocompletion and IntelliSense
- Safer refactoring
- Hover information
- Navigation to definitions
- Automatic imports
3. Self-Documenting Code
Types serve as documentation that stays up-to-date:
// Without types - what does this function expect?
function processUser(user) {
// ...
}
// With types - clear expectations
function processUser(user: {
id: number;
name: string;
email: string;
isActive: boolean;
}) {
// ...
}
// Even better with interfaces
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
function processUser(user: User) {
// ...
}
4. Enhanced Code Quality and Maintainability
TypeScript encourages better architecture and makes it easier to maintain and refactor large codebases.
Getting Started with TypeScript
Let's set up a basic TypeScript project and explore the core concepts.
Installation and Setup
- Install TypeScript globally:
npm install -g typescript
- Create a new project directory and initialize it:
mkdir typescript-demo
cd typescript-demo
npm init -y
- Create a
tsconfig.json
file to configure TypeScript:
tsc --init
This creates a default configuration file. Here's a simplified version:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
- Create a
src
directory for your TypeScript files:
mkdir src
- Create your first TypeScript file
src/index.ts
:
console.log("Hello, TypeScript!");
- Compile and run your code:
tsc
node dist/index.js
Basic Types in TypeScript
TypeScript provides several basic types that you can use to describe your data:
Primitive Types
// Boolean
let isDone: boolean = false;
// Number
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
// String
let color: string = "blue";
let greeting: string = `Hello, my name is ${name}`;
// Null and Undefined
let n: null = null;
let u: undefined = undefined;
// Symbol
let sym: symbol = Symbol("key");
// BigInt
let bigNumber: bigint = 100n;
Arrays
// Array of numbers
let numbers: number[] = [1, 2, 3];
// Alternative syntax
let strings: Array<string> = ["hello", "world"];
// Readonly array
let readonlyNumbers: readonly number[] = [1, 2, 3];
Tuples
Tuples are arrays with a fixed number of elements whose types are known:
// Tuple
let tuple: [string, number] = ["hello", 10];
// Accessing elements
console.log(tuple[0].substring(1)); // "ello"
console.log(tuple[1].toFixed(2)); // "10.00"
// Error: Property 'substring' does not exist on type 'number'
// console.log(tuple[1].substring(1));
Enums
Enums allow you to define a set of named constants:
enum Color {
Red,
Green,
Blue
}
let c: Color = Color.Green;
console.log(c); // 1
// You can also set custom values
enum HttpStatus {
OK = 200,
NotFound = 404,
InternalServerError = 500
}
console.log(HttpStatus.OK); // 200
Any and Unknown
When you don't know the type, you can use any
or unknown
:
// Any - disables type checking
let notSure: any = 4;
notSure = "maybe a string";
notSure = false;
// Unknown - safer alternative to any
let uncertain: unknown = 4;
uncertain = "maybe a string";
uncertain = false;
// Error: 'uncertain' is of type 'unknown'
// uncertain.toFixed();
// Type checking is required before using unknown values
if (typeof uncertain === "number") {
uncertain.toFixed();
}
Void, Never, and Object
// Void - absence of any type (commonly used for functions that don't return a value)
function logMessage(message: string): void {
console.log(message);
}
// Never - represents values that never occur (e.g., a function that always throws an error)
function throwError(message: string): never {
throw new Error(message);
}
// Object - represents any non-primitive type
let obj: object = { key: "value" };
Type Assertions
Sometimes you might have information about a value's type that TypeScript doesn't know about. Type assertions allow you to tell the compiler "trust me, I know what I'm doing":
// Using angle-bracket syntax
let someValue: unknown = "this is a string";
let strLength: number = (<string>someValue).length;
// Using as syntax (preferred in JSX)
let otherValue: unknown = "another string";
let otherLength: number = (otherValue as string).length;
Interfaces
Interfaces define the structure that objects must adhere to:
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
// Optional property
phone?: string;
// Readonly property
readonly createdAt: Date;
}
function createUser(user: User): User {
return user;
}
const newUser: User = {
id: 1,
name: "John Doe",
email: "john@example.com",
isActive: true,
createdAt: new Date()
};
createUser(newUser);
// Error: Missing required properties
// createUser({ name: "John" });
// Error: Cannot assign to 'createdAt' because it is a read-only property
// newUser.createdAt = new Date();
Extending Interfaces
Interfaces can extend other interfaces:
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
employeeId: number;
department: string;
}
const employee: Employee = {
name: "Jane Smith",
age: 32,
employeeId: 123,
department: "Engineering"
};
Type Aliases
Type aliases create a new name for a type:
type Point = {
x: number;
y: number;
};
function printCoord(pt: Point) {
console.log(`Coordinates: ${pt.x}, ${pt.y}`);
}
printCoord({ x: 100, y: 100 });
// Union types
type ID = number | string;
function printID(id: ID) {
console.log(`ID: ${id}`);
}
printID(101);
printID("202");
Functions in TypeScript
TypeScript allows you to specify the types of function parameters and return values:
// Function with parameter and return type annotations
function add(a: number, b: number): number {
return a + b;
}
// Optional parameters
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting}, ${name}!`;
}
return `Hello, ${name}!`;
}
console.log(greet("John")); // "Hello, John!"
console.log(greet("John", "Hi")); // "Hi, John!"
// Default parameters
function createPoint(x: number = 0, y: number = 0): Point {
return { x, y };
}
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// Function types
type MathOperation = (a: number, b: number) => number;
const multiply: MathOperation = (a, b) => a * b;
Classes in TypeScript
TypeScript adds type annotations and other features to JavaScript classes:
class Animal {
// Property with type annotation
name: string;
// Private property (only accessible within the class)
private age: number;
// Protected property (accessible within the class and subclasses)
protected species: string;
// Readonly property
readonly birthDate: Date;
// Constructor
constructor(name: string, age: number, species: string) {
this.name = name;
this.age = age;
this.species = species;
this.birthDate = new Date();
}
// Method
makeSound(): void {
console.log("Some generic sound");
}
// Getter
get animalAge(): number {
return this.age;
}
// Setter
set animalAge(age: number) {
if (age > 0) {
this.age = age;
}
}
}
// Inheritance
class Dog extends Animal {
// Additional property
breed: string;
constructor(name: string, age: number, breed: string) {
super(name, age, "Canine");
this.breed = breed;
}
// Override method
makeSound(): void {
console.log("Woof! Woof!");
}
// Additional method
fetch(): void {
console.log(`${this.name} is fetching...`);
}
}
const dog = new Dog("Rex", 3, "German Shepherd");
dog.makeSound(); // "Woof! Woof!"
console.log(dog.name); // "Rex"
console.log(dog.animalAge); // 3
dog.animalAge = 4;
console.log(dog.animalAge); // 4
// Error: Property 'age' is private and only accessible within class 'Animal'
// console.log(dog.age);
// Error: Property 'species' is protected and only accessible within class 'Animal' and its subclasses
// console.log(dog.species);
// Error: Cannot assign to 'birthDate' because it is a read-only property
// dog.birthDate = new Date();
Generics
Generics allow you to create reusable components that work with a variety of types:
// Generic function
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(42);
const str = identity<string>("hello");
// Type inference works too
const bool = identity(true); // TypeScript infers boolean
// Generic interface
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hello" };
// Generic class
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
}
const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
console.log(numberQueue.dequeue()); // 1
// Generic constraints
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // 5
logLength([1, 2, 3]); // 3
logLength({ length: 10, value: 3 }); // 10
// Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'
// logLength(3);
Advanced Types
TypeScript offers several advanced type features:
Union Types
// Union type
type StringOrNumber = string | number;
function printId(id: StringOrNumber) {
console.log(`ID: ${id}`);
}
printId(101);
printId("202");
Intersection Types
// Intersection type
type Employee = {
id: number;
name: string;
};
type Manager = {
employees: Employee[];
department: string;
};
type ManagerWithEmployeeInfo = Employee & Manager;
const manager: ManagerWithEmployeeInfo = {
id: 1,
name: "Jane Smith",
employees: [{ id: 2, name: "John Doe" }],
department: "Engineering"
};
Type Guards
// Type guard with typeof
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return " ".repeat(padding) + value;
}
return padding + value;
}
// Type guard with instanceof
class Bird {
fly() {
console.log("Flying...");
}
}
class Fish {
swim() {
console.log("Swimming...");
}
}
function move(animal: Bird | Fish) {
if (animal instanceof Bird) {
animal.fly();
} else {
animal.swim();
}
}
// User-defined type guard
function isFish(pet: Bird | Fish): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function makeMove(pet: Bird | Fish) {
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
}
Literal Types
// String literal type
type Direction = "north" | "south" | "east" | "west";
function move(direction: Direction) {
console.log(`Moving ${direction}`);
}
move("north"); // OK
// Error: Argument of type '"northeast"' is not assignable to parameter of type 'Direction'
// move("northeast");
// Numeric literal type
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
function rollDice(): DiceRoll {
return (Math.floor(Math.random() * 6) + 1) as DiceRoll;
}
Nullable Types
// With strictNullChecks enabled
function greet(name: string | null | undefined) {
if (name === null) {
console.log("Hello, anonymous user!");
} else if (name === undefined) {
console.log("Hello there!");
} else {
console.log(`Hello, ${name}!`);
}
}
greet("John"); // "Hello, John!"
greet(null); // "Hello, anonymous user!"
greet(undefined); // "Hello there!"
Modules and Namespaces
TypeScript supports both ES modules and its own namespace system:
ES Modules
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
// main.ts
import { add, subtract } from "./math";
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
Namespaces
// validation.ts
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
export class RegexValidator implements StringValidator {
private regex: RegExp;
constructor(regex: RegExp) {
this.regex = regex;
}
isValid(s: string): boolean {
return this.regex.test(s);
}
}
}
// app.ts
/// <reference path="validation.ts" />
const emailValidator = new Validation.RegexValidator(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
console.log(emailValidator.isValid("test@example.com")); // true
Decorators
Decorators provide a way to add annotations and metadata to class declarations, methods, properties, and parameters:
// Class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
// Method decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
return originalMethod.apply(this, args);
};
return descriptor;
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@log
greet() {
return `Hello, ${this.greeting}`;
}
}
const greeter = new Greeter("world");
console.log(greeter.greet()); // Logs: Calling greet with args: [] \n Hello, world
Integrating TypeScript with Existing JavaScript
TypeScript can be gradually adopted in existing JavaScript projects:
- Add a
tsconfig.json
file - Set
"allowJs": true
to allow JavaScript files - Rename files from
.js
to.ts
one by one - Add type annotations gradually
You can also use declaration files (.d.ts
) to add types to JavaScript libraries:
// jquery.d.ts
declare const $: {
(selector: string): {
text(content: string): void;
on(event: string, handler: (event: any) => void): void;
};
};
// Using jQuery with TypeScript
$("button").on("click", (event) => {
$("#message").text("Button clicked!");
});
Conclusion
TypeScript offers a powerful type system that builds on JavaScript, providing better tooling, earlier error detection, and improved code quality. This introduction covers the basics, but TypeScript has many more features to explore.
As you continue your TypeScript journey, remember these best practices:
- Start with strict type checking enabled
- Use interfaces and type aliases to create reusable types
- Leverage generics for flexible, reusable components
- Add types gradually to existing JavaScript code
- Use the TypeScript compiler options that make sense for your project
TypeScript's learning curve is relatively gentle for JavaScript developers, and the benefits it provides make it well worth the investment. Happy coding!