Syntax: let variable_name: type = value
, const variable_name: type = value
, var variable_name: type = value
In TypeScript, let
and const
are used for declaring variables, while var
is also available (though generally not recommended due to scope issues).
let x: number = 10; // 'let' allows reassignment
const name: string = "Alice"; // 'const' does not allow reassignment
var isActive: boolean = true; // 'var' declares a variable with function-level scope
console.log(x, name, isActive); // Output: 10 Alice true
Syntax: let variable_name: type
TypeScript provides various primitive types to define the type of a variable.
let num: number = 42; // number
let name: string = "John"; // string
let isActive: boolean = true; // boolean
let empty: null = null; // null
let notAssigned: undefined = undefined; // undefined
let uniqueId: symbol = Symbol("id"); // symbol
let largeNum: bigint = 100n; // bigint
console.log(typeof num, typeof name, typeof isActive, typeof empty, typeof notAssigned, typeof uniqueId, typeof largeNum); // Output: number string boolean object undefined symbol bigint
Syntax: let variable_name: type = value
Type annotations allow you to specify the type of a variable explicitly in TypeScript.
let name: string = "Alice"; // 'name' is explicitly declared as a string
let age: number = 25; // 'age' is explicitly declared as a number
console.log(name, age); // Output: Alice 25
Syntax: let variable_name = value
TypeScript can automatically infer the type of a variable based on the value assigned to it, so type annotations are not always required.
let name = "Alice"; // TypeScript infers that 'name' is a string
let age = 25; // TypeScript infers that 'age' is a number
console.log(typeof name, typeof age); // Output: string number
Syntax: value as Type
|
Type assertions tell TypeScript to treat a value as a specific type. This is useful when you know more about the type than TypeScript can infer.
let someValue: any = "This is a string";
let strLength: number = (someValue as string).length; // Using 'as' for type assertion
console.log(strLength); // Output: 17
let anotherValue: any = "Hello, world!";
let strLengthAlt: number = (anotherValue).length; // Using '' for type assertion
console.log(strLengthAlt); // Output: 13
Syntax: `Hello, ${name}!`
Template literals allow you to embed expressions inside string literals using the ${}
syntax. They can span multiple lines and allow string interpolation.
let name: string = "Alice";
let greeting: string = `Hello, ${name}!`; // Using template literals
console.log(greeting); // Output: Hello, Alice!
let multiLineString: string = `This is a string
that spans multiple
lines.`;
console.log(multiLineString); // Output: This is a string
that spans multiple
lines.
Syntax: { key: type; key: type; }
In TypeScript, you can define an object with specific types for each property. This ensures that each property is of the expected type.
let person: { name: string; age: number } = { name: "Alice", age: 30 };
console.log(person); // Output: { name: "Alice", age: 30 }
console.log(person.name); // Output: Alice
console.log(person.age); // Output: 30
Syntax: number[]
or Array
Arrays in TypeScript can be defined using either the array literal []
or the Array
syntax. Both are valid ways to define arrays.
let nums: number[] = [1, 2, 3, 4, 5]; // Using array literal syntax
let names: Array = ["Alice", "Bob", "Charlie"]; // Using Array syntax
console.log(nums); // Output: [1, 2, 3, 4, 5]
console.log(names); // Output: ["Alice", "Bob", "Charlie"]
Syntax: [type, type]
Tuples in TypeScript are arrays with a fixed number of elements, each of which has a specific type.
let person: [string, number] = ["Alice", 30]; // Tuple with a string and a number
console.log(person); // Output: ["Alice", 30]
console.log(person[0]); // Output: Alice
console.log(person[1]); // Output: 30
Syntax: readonly type[]
Readonly arrays in TypeScript ensure that the array cannot be modified after its creation. Any attempt to modify it will result in a compile-time error.
let fruits: readonly string[] = ["Apple", "Banana", "Cherry"];
console.log(fruits); // Output: ["Apple", "Banana", "Cherry"]
// fruits.push("Orange"); // Error: Property 'push' does not exist on type 'readonly string[]'
Syntax: { [key: string]: type }
Index signatures allow objects to have dynamic keys with a specified type for the values. This is useful when you don’t know the exact keys ahead of time.
let scores: { [key: string]: number } = { "Alice": 90, "Bob": 85 };
console.log(scores); // Output: { Alice: 90, Bob: 85 }
console.log(scores["Alice"]); // Output: 90
console.log(scores["Bob"]); // Output: 85
Syntax: (x: type, y: type) => returnType
Function parameters can be typed in TypeScript to specify what type of value is expected. The return type is also specified after the arrow (=>
).
const add = (x: number, y: number): number => {
return x + y;
};
console.log(add(3, 4)); // Output: 7
Syntax: x?: type
Optional parameters are marked with a ?
, which makes it optional for the caller to provide a value for the parameter.
const greet = (name: string, age?: number): string => {
return age ? `Hello ${name}, you are ${age} years old.` : `Hello ${name}`;
};
console.log(greet("Alice", 25)); // Output: Hello Alice, you are 25 years old.
console.log(greet("Bob")); // Output: Hello Bob
Syntax: x = value
Default parameters allow a function to use a default value if no value is provided by the caller.
const multiply = (x: number = 10, y: number): number => {
return x * y;
};
console.log(multiply(5, 2)); // Output: 10
console.log(multiply(3)); // Output: 30
Syntax: ...args: type[]
Rest parameters allow a function to accept an arbitrary number of arguments, which are stored in an array.
const sum = (...args: number[]): number => {
return args.reduce((acc, num) => acc + num, 0);
};
console.log(sum(1, 2, 3, 4)); // Output: 10
console.log(sum(5, 10)); // Output: 15
Syntax: (): returnType
Return type annotations are used to specify the type of value that a function returns. This helps TypeScript ensure that the function returns the correct type.
const getName = (): string => {
return "Alice";
};
console.log(getName()); // Output: Alice
Syntax: const fn = () => {}
Arrow functions provide a shorter syntax for writing functions. They also retain the this
context from the surrounding code.
const square = (x: number): number => {
return x * x;
};
console.log(square(4)); // Output: 16
Syntax: interface InterfaceName { property: type }
Interfaces define the structure of an object, specifying the names and types of properties it should have.
interface User {
name: string;
age: number;
}
const user: User = {
name: "Alice",
age: 30,
};
console.log(user); // Output: { name: 'Alice', age: 30 }
Syntax: type TypeName = { property: type }
Type aliases are used to create a new name for a type, which can be an object, union, or any valid TypeScript type.
type Point = {
x: number;
y: number;
}
const point: Point = {
x: 5,
y: 10,
};
console.log(point); // Output: { x: 5, y: 10 }
Syntax: interface Child extends Parent { property: type }
Interfaces can be extended using the extends
keyword, allowing one interface to inherit properties from another.
interface User {
name: string;
age: number;
}
interface Admin extends User {
role: string;
}
const admin: Admin = {
name: "Bob",
age: 40,
role: "Manager",
};
console.log(admin); // Output: { name: 'Bob', age: 40, role: 'Manager' }
Syntax: A & B
Intersection types allow combining multiple types into one. A value of this type must satisfy all types.
type Person = {
name: string;
age: number;
}
type Address = {
street: string;
city: string;
}
type PersonWithAddress = Person & Address;
const person: PersonWithAddress = {
name: "Charlie",
age: 35,
street: "123 Main St",
city: "New York",
};
console.log(person); // Output: { name: 'Charlie', age: 35, street: '123 Main St', city: 'New York' }
Syntax: typeA | typeB
Union types allow a variable to hold multiple types. A value of this type can be one of the specified types.
let value: string | number;
value = "Hello";
console.log(value); // Output: Hello
value = 42;
console.log(value); // Output: 42
Syntax: type "value1" | "value2"
Literal types are used to specify the exact value a variable can have, rather than just a type.
type Status = "success" | "error";
let status: Status;
status = "success";
console.log(status); // Output: success
status = "error";
console.log(status); // Output: error
Syntax: class ClassName { ... }
Classes in TypeScript are blueprints for creating objects. A class can contain properties, methods, and a constructor.
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, ${this.name}!`);
}
}
const person = new Person("Alice");
person.greet(); // Output: Hello, Alice!
Syntax: constructor(parameter: type) { ... }
The constructor is a special method used to initialize objects created from a class. It is called automatically when an instance is created.
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}
const person = new Person("Bob");
console.log(person.name); // Output: Bob
Syntax: public propertyName: type
, private propertyName: type
, protected propertyName: type
Properties in a class can have different access modifiers:
class Person {
public name: string;
private age: number;
protected address: string;
constructor(name: string, age: number, address: string) {
this.name = name;
this.age = age;
this.address = address;
}
}
const person = new Person("Charlie", 30, "123 Main St");
console.log(person.name); // Output: Charlie
// console.log(person.age); // Error: Property 'age' is private
Syntax: readonly propertyName: type
The readonly
modifier ensures that the property cannot be reassigned after it is initialized.
class Product {
readonly id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
const product = new Product(1, "Laptop");
console.log(product.id); // Output: 1
// product.id = 2; // Error: Cannot assign to 'id' because it is a read-only property
Syntax: get propertyName(): type
and set propertyName(value: type)
Getters and setters allow you to define custom behavior when accessing or modifying a property.
class Person {
private _name: string;
constructor(name: string) {
this._name = name;
}
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
const person = new Person("Dave");
console.log(person.name); // Output: Dave
person.name = "Eve";
console.log(person.name); // Output: Eve
Syntax: class Child extends Parent
Inheritance allows a class to inherit properties and methods from another class, known as the parent class.
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
speak() {
console.log(`${this.name} barks.`);
}
}
const dog = new Dog("Rex");
dog.speak(); // Output: Rex barks.
Syntax: abstract class ClassName { ... }
Abstract classes cannot be instantiated directly and are used to define a base class that other classes can inherit from.
abstract class Animal {
abstract sound(): void;
}
class Dog extends Animal {
sound() {
console.log("Bark!");
}
}
const dog = new Dog();
dog.sound(); // Output: Bark!
// const animal = new Animal(); // Error: Cannot instantiate an abstract class
Syntax: class ClassName implements InterfaceName
The implements
keyword is used to ensure that a class follows the structure of an interface.
interface Vehicle {
start(): void;
stop(): void;
}
class Car implements Vehicle {
start() {
console.log("Car started.");
}
stop() {
console.log("Car stopped.");
}
}
const car = new Car();
car.start(); // Output: Car started.
car.stop(); // Output: Car stopped.
Syntax: enum EnumName { member1, member2, ... }
Numeric enums allow you to define a set of named numeric values. By default, the first member starts at 0, and the rest are incremented by 1.
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
console.log(Direction.Up); // Output: 0
console.log(Direction.Down); // Output: 1
console.log(Direction.Left); // Output: 2
console.log(Direction.Right); // Output: 3
Syntax: enum EnumName { member1 = "value1", member2 = "value2" }
String enums allow you to define a set of named string values. Each member is assigned a specific string value.
enum Status {
Success = "SUCCESS",
Failure = "FAILURE",
Pending = "PENDING"
}
console.log(Status.Success); // Output: SUCCESS
console.log(Status.Failure); // Output: FAILURE
console.log(Status.Pending); // Output: PENDING
Syntax: enum EnumName { member1 = value1, member2 = value2 }
Heterogeneous enums allow a combination of numeric and string values within the same enum. This can be useful for scenarios where you need both types of values.
enum Mixed {
No = 0,
Yes = "YES"
}
console.log(Mixed.No); // Output: 0
console.log(Mixed.Yes); // Output: YES
Syntax: function functionName<T>(arg: T): T
Generic functions allow you to define a function that works with any data type. The type is specified as a placeholder <T>, which will be replaced with a specific type when the function is called.
function identity<T>(arg: T): T {
return arg;
}
console.log(identity(5)); // Output: 5
console.log(identity("Hello")); // Output: Hello
Syntax: interface InterfaceName<T> { property: T }
Generic interfaces allow you to define an interface with a placeholder for a type. This can be used to enforce that specific properties in objects conform to a specific type.
interface Box<T> {
value: T;
}
const box1: Box<number> = { value: 42 };
const box2: Box<string> = { value: "Hello" };
console.log(box1.value); // Output: 42
console.log(box2.value); // Output: Hello
Syntax: class ClassName<T> { ... }
Generic classes allow you to create classes that work with any data type. The placeholder type <T> will be replaced when the class is instantiated.
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
}
const queue = new Queue<number>();
queue.enqueue(10);
queue.enqueue(20);
console.log(queue.dequeue()); // Output: 10
Syntax: <T extends SomeType>
Constraints allow you to specify that the placeholder type <T> must extend or be a subtype of a given type. This ensures that only compatible types can be used with the generic code.
function merge<T extends object>(obj1: T, obj2: T): T {
return { ...obj1, ...obj2 };
}
const obj1 = { name: "Alice", age: 25 };
const obj2 = { city: "New York" };
console.log(merge(obj1, obj2)); // Output: { name: 'Alice', age: 25, city: 'New York' }
Syntax: Partial<T>
Partial makes all properties of a given type optional. It is useful when you need to work with an object but don’t want to specify all its properties.
interface Person {
name: string;
age: number;
}
const person: Partial<Person> = { name: "Alice" };
console.log(person); // Output: { name: 'Alice' }
Syntax: Required<T>
Required makes all properties of a given type required. It is useful when you need to ensure that all properties of an object are present.
interface Person {
name?: string;
age?: number;
}
const person: Required<Person> = { name: "Alice", age: 25 };
console.log(person); // Output: { name: 'Alice', age: 25 }
Syntax: Readonly<T>
Readonly makes all properties of a given type immutable (readonly). You cannot modify the properties of an object once it is defined.
interface Person {
name: string;
age: number;
}
const person: Readonly<Person> = { name: "Alice", age: 25 };
// person.age = 26; // Error: Cannot assign to 'age' because it is a read-only property.
console.log(person); // Output: { name: 'Alice', age: 25 }
Syntax: Record<K, T>
Record constructs an object type with a specific set of keys (K) and values of type (T). It is useful for creating maps or dictionaries.
type Score = Record<string, number>;
const scores: Score = { Alice: 90, Bob: 85 };
console.log(scores.Alice); // Output: 90
Syntax: Pick<T, K>
Pick allows you to select a subset of properties from a given type (T). You specify the properties to pick using type K.
interface Person {
name: string;
age: number;
email: string;
}
type PersonName = Pick<Person, "name" | "email">;
const person: PersonName = { name: "Alice", email: "alice@example.com" };
console.log(person); // Output: { name: 'Alice', email: 'alice@example.com' }
Syntax: Omit<T, K>
Omit creates a type that excludes specific properties from the given type (T). You specify the properties to exclude using type K.
interface Person {
name: string;
age: number;
email: string;
}
type PersonWithoutEmail = Omit<Person, "email">;
const person: PersonWithoutEmail = { name: "Alice", age: 25 };
console.log(person); // Output: { name: 'Alice', age: 25 }
Syntax: Exclude<T, U>
Exclude removes types from a union type. It excludes types from the first type (T) that are present in the second type (U).
type T = string | number | boolean;
type U = Exclude<T, boolean>;
const value: U = "Hello";
console.log(value); // Output: Hello
Syntax: Extract<T, U>
Extract creates a type that extracts the types from the union (T) that are assignable to (U).
type T = string | number | boolean;
type U = Extract<T, string | boolean>;
const value: U = "Hello";
console.log(value); // Output: Hello
Syntax: NonNullable<T>
NonNullable removes null and undefined from a given type (T), ensuring that the type cannot be null or undefined.
type T = string | number | null | undefined;
type NonNull = NonNullable<T>;
const value: NonNull = "Hello";
console.log(value); // Output: Hello
Syntax: typeof
, instanceof
Type Guards allow you to narrow down the type of a variable within a block of code. You can use typeof
for primitive types or instanceof
for class instances.
function printLength(value: string | number): void {
if (typeof value === "string") {
console.log(value.length); // Only works if value is a string
} else {
console.log(value.toFixed(2)); // Only works if value is a number
}
}
printLength("Hello"); // Output: 5
printLength(42); // Output: 42.00
Syntax: union types
with a common literal property (discriminator).
Discriminated Unions are a pattern used to narrow down types. A common property (discriminator) is used to distinguish between types in a union.
interface Bird {
type: "bird";
flySpeed: number;
}
interface Fish {
type: "fish";
swimSpeed: number;
}
type Animal = Bird | Fish;
function move(animal: Animal): void {
if (animal.type === "bird") {
console.log(`Flying at ${animal.flySpeed} km/h`);
} else {
console.log(`Swimming at ${animal.swimSpeed} km/h`);
}
}
move({ type: "bird", flySpeed: 20 }); // Output: Flying at 20 km/h
move({ type: "fish", swimSpeed: 5 }); // Output: Swimming at 5 km/h
Syntax: "property" in object
The in
operator is used to check if a property exists in an object. It can be used as a type guard to narrow down types based on the presence of certain properties.
interface Bird {
flySpeed: number;
}
interface Fish {
swimSpeed: number;
}
type Animal = Bird | Fish;
function move(animal: Animal): void {
if ("flySpeed" in animal) {
console.log(`Flying at ${animal.flySpeed} km/h`);
} else {
console.log(`Swimming at ${animal.swimSpeed} km/h`);
}
}
move({ flySpeed: 20 }); // Output: Flying at 20 km/h
move({ swimSpeed: 5 }); // Output: Swimming at 5 km/h
Syntax: function is
Type Predicates are used to tell TypeScript the type of a variable inside a function. They provide a way to refine the type in the scope of a conditional block.
interface Fish {
swimSpeed: number;
}
function isFish(pet: any): pet is Fish {
return (pet as Fish).swimSpeed !== undefined;
}
function move(pet: Fish | { name: string }): void {
if (isFish(pet)) {
console.log(`Swimming at ${pet.swimSpeed} km/h`);
} else {
console.log(`${pet.name} is not a fish`);
}
}
move({ swimSpeed: 10 }); // Output: Swimming at 10 km/h
move({ name: "Charlie" }); // Output: Charlie is not a fish
Syntax: export
The export
keyword allows you to export variables, functions, classes, or objects from a module so they can be used in other files.
// file1.ts
export const x = 1;
// file2.ts
import { x } from "./file1";
console.log(x); // Output: 1
Syntax: export default
The export default
keyword is used to export a single value from a module, such as a class, function, or object, which can be imported without using curly braces.
// file1.ts
export default class Person {
constructor(public name: string) {}
}
// file2.ts
import Person from "./file1";
const person = new Person("John");
console.log(person.name); // Output: John
Syntax: import { x } from "./file"
The import
keyword is used to bring in modules, functions, variables, or classes from other files. You must use the same name as the exported element when importing (unless using a default export).
// file1.ts
export const x = 10;
// file2.ts
import { x } from "./file1";
console.log(x); // Output: 10
Syntax: import("./file")
Dynamic imports allow you to load modules on demand. This helps with lazy loading, as the module will only be loaded when it's needed. It returns a promise.
// file1.ts
export const x = 100;
// file2.ts
async function loadModule() {
const module = await import("./file1");
console.log(module.x); // Output: 100
}
loadModule();
Syntax: { [P in K]: T }
Mapped types allow you to create new types by transforming properties of an existing type. You can iterate over keys and define how their values should be transformed.
type Keys = "name" | "age";
type Person = { name: string; age: number; }
type ReadOnlyPerson = { [P in Keys]: string };
const person: ReadOnlyPerson = { name: "John", age: "30" };
Syntax: T extends U ? X : Y
Conditional types allow you to define types based on a condition. If the condition is true, the type is X
; otherwise, it is Y
.
type IsString = T extends string ? "Yes" : "No";
type Test1 = IsString; // "Yes"
type Test2 = IsString; // "No"
Syntax: type Event = "click" | "hover"
Template literal types allow you to create types that are built from string literals, including combinations of literals and expressions.
type Event = "click" | "hover";
type ButtonEvent = `button-${Event}`;
const clickEvent: ButtonEvent = "button-click"; // Valid
const invalidEvent: ButtonEvent = "button-tap"; // Error: Type '"button-tap"' is not assignable to type 'ButtonEvent'.
Syntax: infer
The infer
keyword is used in conditional types to infer a type within the scope of a condition. It is often used to extract the return type of functions.
type ReturnType = T extends (...args: any) => infer R ? R : any;
type MyFunction = (x: number, y: string) => boolean;
type FunctionReturn = ReturnType; // boolean
Syntax: T extends U ? X : Y
Distributive conditional types apply the conditional type logic to each element of a union type.
type ExtractStrings = T extends string ? T : never;
type Result = ExtractStrings<"a" | "b" | 1 | 2>; // "a" | "b"
Syntax: type Event = `${string}-${number}`
Template literal types combined with conditional types allow more flexible and dynamic type creation.
type Event = `${string}-${number}`;
type IsEvent = T extends `${string}-${number}` ? "Valid" : "Invalid";
type Test1 = IsEvent<"click-1">; // "Valid"
type Test2 = IsEvent<"error">; // "Invalid"
Syntax: type Tree
Recursive types allow you to define types that refer to themselves, useful for structures like trees.
type Tree = { value: T; children?: Tree[] };
const tree: Tree = { value: "root", children: [{ value: "child", children: [] }] };
Syntax: A & B
Intersection types combine multiple types into one, requiring an object to satisfy all the types in the intersection.
type A = { name: string };
type B = { age: number };
type AB = A & B;
const ab: AB = { name: "John", age: 30 };
Syntax: T | U
Union types allow a value to be one of several types. The value can be any one of the types in the union.
type A = string | number;
const a: A = "Hello";
const b: A = 42;
Syntax: try { ... } catch (e: unknown) { ... }
The try/catch
statement allows you to catch runtime errors. In TypeScript, you should use unknown
for the error type instead of any
, to ensure type safety.
try {
throw new Error("Something went wrong!");
} catch (e: unknown) {
if (e instanceof Error) {
console.log(e.message);
} else {
console.log("Unknown error");
}
}
Syntax: class MyError extends Error { ... }
Custom errors allow you to define more specific errors tailored to your application. You can create a class that extends the built-in Error
class.
class MyError extends Error {
constructor(message: string) {
super(message);
this.name = "MyError";
}
}
function test() {
throw new MyError("This is a custom error!");
}
try {
test();
} catch (e: unknown) {
if (e instanceof MyError) {
console.log(e.message);
} else {
console.log("Unknown error");
}
}
Syntax: try { ... } catch (e: unknown) { ... }
with async/await
Handling errors in asynchronous code with async/await
is done in the same way as synchronous code, but you need to ensure you are working with promises.
async function fetchData() {
const response = await fetch("https://api.example.com");
if (!response.ok) {
throw new Error("Failed to fetch data");
}
return await response.json();
}
async function handleData() {
try {
const data = await fetchData();
console.log(data);
} catch (e: unknown) {
if (e instanceof Error) {
console.log(e.message);
} else {
console.log("Unknown error");
}
}
}
handleData();
Syntax: throw e
Sometimes, after catching an error, you may need to rethrow it, possibly to be handled elsewhere. This is commonly used in logging or when transforming the error.
function processData(data: string) {
if (data.length === 0) {
throw new Error("Data cannot be empty");
}
return data;
}
function handleError() {
try {
processData("");
} catch (e: unknown) {
console.log("Logging error: ", e);
throw e; // Rethrow the error to be handled at a higher level.
}
}
try {
handleError();
} catch (e: unknown) {
if (e instanceof Error) {
console.log("Final handling of the error:", e.message);
}
}
Syntax: static getDerivedStateFromError()
, componentDidCatch()
In React, error boundaries are used to catch JavaScript errors anywhere in the component tree and log those errors, displaying a fallback UI.
import React, { Component, ErrorInfo } from 'react';
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error: Error) {
return { hasError: true };
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.log("Error occurred:", error, info);
}
render() {
if (this.state.hasError) {
return Something went wrong!
;
}
return this.props.children;
}
}
export default ErrorBoundary;
Syntax: class CustomError extends Error { ... }
In some cases, it’s useful to include error codes alongside error messages for better error handling. You can extend the custom error class to include a code.
class CustomError extends Error {
code: number;
constructor(message: string, code: number) {
super(message);
this.name = "CustomError";
this.code = code;
}
}
function throwError() {
throw new CustomError("Something went wrong!", 1001);
}
try {
throwError();
} catch (e: unknown) {
if (e instanceof CustomError) {
console.log(`Error ${e.code}: ${e.message}`);
} else {
console.log("Unknown error");
}
}
Syntax: new Promise((resolve, reject) => { ... })
A Promise
represents the eventual completion (or failure) of an asynchronous operation. It has three states: pending, resolved (fulfilled), and rejected.
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched!");
}, 1000);
});
fetchData.then(result => {
console.log(result);
}).catch(error => {
console.log(error);
});
Syntax: async function functionName() { await someAsyncOperation() }
async/await
is a cleaner way to work with asynchronous code. The async
keyword makes a function return a promise, and the await
keyword pauses the execution of the function until the promise is resolved.
async function fetchData() {
const result = await new Promise<string>((resolve) => {
setTimeout(() => {
resolve("Data fetched with async/await!");
}, 1000);
});
console.log(result);
}
fetchData();
Syntax: promise.then().then().catch()
Promises can be chained to execute asynchronous operations sequentially. Each then
block handles the resolved value from the previous one.
function fetchData() {
return new Promise<string>((resolve) => {
setTimeout(() => {
resolve("Data fetched!");
}, 1000);
});
}
fetchData()
.then(result => {
console.log(result);
return "Processed Data";
})
.then(processed => {
console.log(processed);
})
.catch(error => {
console.log("Error:", error);
});
Syntax: try { await someAsyncOperation() } catch (error) { ... }
In async functions, you can handle errors using a try/catch
block. This is the recommended way of handling errors when working with async/await.
async function fetchData() {
try {
const result = await new Promise<string>((resolve, reject) => {
setTimeout(() => {
reject("Failed to fetch data");
}, 1000);
});
console.log(result);
} catch (error) {
console.log("Error:", error);
}
}
fetchData();
Syntax: Promise.all([promise1, promise2])
When you need to perform multiple asynchronous operations in parallel, you can use Promise.all
, which waits for all promises to resolve and returns their results.
async function fetchData() {
const promise1 = new Promise<string>((resolve) => {
setTimeout(() => resolve("Data 1 fetched"), 1000);
});
const promise2 = new Promise<string>((resolve) => {
setTimeout(() => resolve("Data 2 fetched"), 500);
});
const results = await Promise.all([promise1, promise2]);
console.log(results);
}
fetchData();
Syntax: for await (const item of asyncIterable)
Async iteration allows you to loop through an asynchronous iterable (like an async generator) using the for await ... of
loop. This is helpful when dealing with asynchronous data sources such as streams.
async function* generateData() {
yield "Data 1";
yield "Data 2";
yield "Data 3";
}
async function fetchData() {
for await (const data of generateData()) {
console.log(data);
}
}
fetchData();
In asynchronous programming, performance can be impacted by how you manage the concurrency of your async operations. Consider the following:
Promise.all
for parallel operations when tasks are independent.async/await
when tasks depend on the result of others.for await ... of
for handling async data streams or large data sets efficiently.Example of performance bottleneck:
async function fetchData() {
const result1 = await new Promise<string>((resolve) => {
setTimeout(() => resolve("Data 1"), 1000);
});
const result2 = await new Promise<string>((resolve) => {
setTimeout(() => resolve("Data 2"), 1000);
});
console.log(result1, result2);
}
fetchData();
In asynchronous code, error handling is crucial. You can handle errors in promise chains with catch
or inside async functions using try/catch
.
function fetchData() {
return new Promise<string>((resolve, reject) => {
setTimeout(() => {
reject("Failed to fetch data");
}, 1000);
});
}
fetchData()
.then(result => {
console.log(result);
})
.catch(error => {
console.log("Error:", error);
});
Syntax: @sealed
A class decorator is applied to the constructor of a class. This decorator is used to modify or extend the behavior of a class. In the example below, the decorator prevents further subclassing of the class by sealing it.
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Person {
constructor(public name: string) {
this.name = name;
}
}
const p = new Person("John");
console.log(p);
// Person cannot be subclassed further
Syntax: @logMethod
Method decorators are applied to methods of a class. They can be used to log method calls, add validation, or track method executions.
function logMethod(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Method ${key} was called with args: ${args}`);
return originalMethod.apply(this, args);
};
}
class User {
@logMethod
greet(name: string) {
return `Hello, ${name}!`;
}
}
const user = new User();
console.log(user.greet("Alice"));
Syntax: @format("Hello")
Property decorators are applied to the properties of a class. These can be used for purposes like formatting values or validating properties before they're set.
function format(value: string) {
return function(target: any, key: string) {
let _val = value;
const getter = () => _val;
const setter = (newVal: string) => {
_val = `${value} ${newVal}`;
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
}
class User {
@format("Hello")
name: string = "";
}
const user = new User();
user.name = "Alice";
console.log(user.name);
Syntax: @logParam
Parameter decorators allow you to add logic before or after a parameter is passed to a method. This is useful for logging or modifying input data before the method execution.
function logParam(target: any, methodName: string, parameterIndex: number) {
console.log(`Parameter at index ${parameterIndex} in method ${methodName} was decorated`);
}
class User {
greet(@logParam name: string) {
return `Hello, ${name}`;
}
}
const user = new User();
user.greet("Bob");
Syntax: @observable
Accessor decorators allow modification of getters and setters in a class. In the example below, the decorator tracks when the property is accessed.
function observable(target: any, key: string, descriptor: PropertyDescriptor) {
const originalGet = descriptor.get;
descriptor.get = function () {
console.log(`Property ${key} was accessed`);
return originalGet?.call(this);
};
}
class User {
private _name = "John";
@observable
get name() {
return this._name;
}
set name(name: string) {
this._name = name;
}
}
const user = new User();
console.log(user.name);
Composite decorators can be used to apply multiple behaviors to a single class or method. By chaining multiple decorators, you can add various functionalities.
function readOnly(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
}
function logMethod(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Method ${key} was called with args: ${args}`);
return originalMethod.apply(this, args);
};
}
class User {
@readOnly
@logMethod
name = "John";
}
const user = new User();
user.name = "Alice"; // Error: cannot assign to read-only property
console.log(user.name);
Decorator factories allow you to pass arguments into your decorators. This lets you create more flexible and reusable decorators.
function logWithMessage(message: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`${message}: ${args}`);
return originalMethod.apply(this, args);
};
};
}
class User {
@logWithMessage("Calling greet method")
greet(name: string) {
return `Hello, ${name}`;
}
}
const user = new User();
console.log(user.greet("Alice"));
The tsconfig.json file is the configuration file that helps control the TypeScript compiler's behavior. It specifies various compiler options such as target output version, module system, file inclusion/exclusion, and more.
Purpose: Controls the TypeScript compiler's behavior.
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
ESLint is a tool used to ensure your code follows a consistent style and catches common programming errors, while Prettier is a code formatter that automatically formats your code to meet your style preferences.
Purpose: Linting (ESLint) and automatic code formatting (Prettier) for TypeScript projects.
// .eslintrc.json
{
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"env": {
"browser": true,
"node": true
},
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "double"]
}
}
// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all"
}
Purpose: Provides type definitions for React, enabling a seamless TypeScript experience when working with React.
// Install dependencies
npm install react react-dom
npm install --save-dev @types/react @types/react-dom
// Example Component
import React, { useState } from 'react';
interface Props {
name: string;
}
const Greeting: React.FC = ({ name }) => {
const [count, setCount] = useState(0);
return (
<div>
<h1>Hello, {name}!</h1>
<button onClick={() => setCount(count + 1)}>Click me: {count}</button>
</div>
);
};
export default Greeting;
Type Definitions for React: @types/react provides TypeScript with the necessary type information to work with React.
React.FC: Type-safe function components.
useState with Types: Specifies that count is of type number, ensuring type safety.
Advanced Topics in React with TypeScript:
UseContext with TypeScript: Providing type-safe context values and consuming them within components.
Higher-Order Components (HOCs): Creating reusable component logic with types.
Generics in React Components: Using generics to make reusable components with type safety.
TypeScript with Webpack: Webpack can be used with TypeScript for module bundling and optimization.
Purpose: Integrate TypeScript with Webpack for efficient bundling and optimization of TypeScript files.
// Install necessary dependencies
npm install --save-dev typescript ts-loader webpack webpack-cli
// webpack.config.js
module.exports = {
entry: './src/index.ts',
output: {
filename: 'bundle.js',
path: __dirname + '/dist'
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
}
};
TypeScript with Babel: Using Babel alongside TypeScript can improve build speed, especially when using features like React JSX.
Purpose: Use Babel to compile TypeScript and JSX code for faster builds.
// Install dependencies
npm install --save-dev @babel/core @babel/preset-env @babel/preset-typescript babel-loader
// babel.config.json
{
"presets": ["@babel/preset-env", "@babel/preset-typescript", "@babel/preset-react"]
}