let firstName: string = "Dylan";
Explicit type assignments are easier to read and more intentional.
let firstName = "Dylan";
With implicit type assignment TypeScript will "guess" the type, based on the assigned value.
let firstName: string = "Alice";
let age: number = 25;
let price: number = 99.99;
let hex: number = 0xff; // Hexadecimal
let binary: number = 0b1010; // Binary
let octal: number = 0o744; // Octal
let isDone: boolean = true;
let bigNumber: bigint = 9007199254740991n; // Adding 'n' makes it a bigint
let anotherBigInt: bigint = BigInt(12345678901234567890);
let uniqueKey: symbol = Symbol("key");
let emptyValue: null = null;
let notAssigned: undefined;
let person: { name: string; age: number } = {
name: "Alice",
age: 25,
};
An object type defines the shape of an object, specifying its properties and their types.
type Person = {
name: string;
age: number;
};
let user: Person = { name: "Bob", age: 30 };
Instead of defining the object inline, you can create a type alias for reuse.
interface Car {
brand: string;
year: number;
}
let myCar: Car = { brand: "Toyota", year: 2022 };
An interface is similar to a type, but it can be extended and implemented in classes. Interfaces are preferred for defining object shapes, especially in OOP.
interface User {
readonly id: number; // Cannot be changed
name: string;
age?: number; // Optional
}
let user1: User = { id: 1, name: "Charlie" };
user1.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.
Optional properties (?) make a property optional. Readonly properties (readonly) prevent modification.
interface Address {
city: string;
country: string;
}
interface Employee {
name: string;
address: Address; // Nested object
}
let emp: Employee = {
name: "David",
address: {
city: "New York",
country: "USA",
},
};
console.log(emp.address.city); // Output: New York
Objects can have other objects inside them.
interface Calculator {
add(a: number, b: number): number;
}
let myCalc: Calculator = {
add: (x, y) => x + y,
};
console.log(myCalc.add(5, 10)); // Output: 15
Objects can have methods as properties.
interface Animal {
species: string;
}
interface Dog extends Animal {
breed: string;
}
let myDog: Dog = { species: "Canine", breed: "Labrador" };
You can extend an interface to create a new one.
type A = { x: number };
type B = { y: number };
type AB = A & B; // Combined type
let point: AB = { x: 10, y: 20 };
Instead of extending, you can combine types using &.
type StudentGrades = Record<string, number>;
let grades: StudentGrades = {
Alice: 90,
Bob: 85,
};
Record<K, V> utility type defines an object type where keys (K) map to values (V).
type Person = {
name: string;
age: number;
email: string;
};
type Employee = {
id: number;
department: string;
};
// Combine Person and Employee but remove `email`
type EmployeeWithoutEmail = Omit<Person, "email"> & Employee;
let employee: EmployeeWithoutEmail = {
name: "Alice",
age: 30,
id: 101,
department: "Engineering",
};
Omit
type EmployeeWithoutEmail = Pick<Person, "name" | "age"> & Employee;
This method is useful when you want to be explicit about the properties you keep.
let numbers: number[] = [1, 2, 3, 4, 5]; // Using []
let names: Array<string> = ["Alice", "Bob", "Charlie"]; // Using Array<T>
Both approaches are valid, but number[] is more commonly used.
const colors: readonly string[] = ["red", "green", "blue"];
colors.push("yellow"); // Error: Cannot modify a readonly array.
Use readonly to prevent modification
let nums: number[] = [10, 20, 30];
nums.push(40); // Allowed
nums.push("50"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
TypeScript enforces type safety on array methods.
let person: [string, number] = ["Alice", 25];
console.log(person[0]); // Output: Alice
console.log(person[1]); // Output: 25
A tuple is an array with a fixed length and specific types for each position.
let user: [string, number?] = ["Bob"];
console.log(user[1]); // Output: undefined
You can make tuple elements optional.
const coordinates: readonly [number, number] = [10, 20];
coordinates[0] = 30; // Error: Cannot assign to index of a readonly tuple.
Tuples can also be readonly.
type StringNumberPair = [string, ...number[]];
let data: StringNumberPair = ["scores", 85, 90, 78];
console.log(data); // Output: ["scores", 85, 90, 78]
Tuples can include rest parameters to allow variable-length data. First element must be a string, but remaining elements can be number[].
let userInfo: [name: string, age: number] = ["Eve", 29];
console.log(userInfo[0]); // Output: Eve
console.log(userInfo[1]); // Output: 29
Named elements in tuples improve readability but don't change functionality.
let value: string | number;
value = "Hello"; // Allowed
value = 42; // Allowed
value = true; // Error: Type 'boolean' is not assignable to 'string | number'.
A union type allows a variable to hold one of multiple types. This is useful when a variable can have different types in different situations.
type Dog = { bark: () => void };
type Cat = { meow: () => void };
let pet: Dog | Cat;
pet = { bark: () => console.log("Woof!") }; // Allowed
pet = { meow: () => console.log("Meow!") }; // Allowed
You can define a union of object types.
function makeSound(animal: Dog | Cat) {
if ("bark" in animal) {
animal.bark(); // TypeScript knows it's a Dog
} else {
animal.meow(); // TypeScript knows it's a Cat
}
}
makeSound({ bark: () => console.log("Woof!") }); // Output: Woof!
makeSound({ meow: () => console.log("Meow!") }); // Output: Meow!
To narrow down a union type, you can use type guards. Using in ensures we call the right method.
type Person = { name: string };
type Employee = { company: string };
type Worker = Person & Employee; // Must have both properties
let worker: Worker = {
name: "Alice",
company: "Tech Corp",
};
An intersection type combines multiple types into one, meaning an object must satisfy all combined types. Worker has properties from both Person and Employee.
type A = { id: string };
type B = { id: number };
type C = A & B; // Error: Type 'string' and 'number' are incompatible.
If the same property exists in multiple types with different types, TypeScript will throw an error. Solution: Ensure the types are compatible or use union (|) instead.
interface HasName {
name: string;
}
interface HasAge {
age: number;
}
type PersonDetails = HasName & HasAge;
let user: PersonDetails = { name: "Bob", age: 30 };
Intersections are useful for merging multiple interfaces. Works like multiple inheritance in object-oriented programming.
enum Direction {
Up,
Down,
Left,
Right,
}
let move: Direction = Direction.Up;
console.log(move); // Output: 0
An enum (enumeration) is a way to give names to a set of related values (usually numbers or strings). This makes the code more meaningful and maintainable. In the example above Direction.Up is 0, Direction.Down is 1, and so on.
enum Status {
Pending, // 0
Approved, // 1
Rejected, // 2
}
console.log(Status.Pending); // Output: 0
console.log(Status.Approved); // Output: 1
Numeric enums automatically assign values starting from 0.
enum Status {
Pending = 1,
Approved = 5,
Rejected = 10,
}
console.log(Status.Pending); // Output: 1
console.log(Status.Approved); // Output: 2
console.log(Status.Rejected); // Output: 3
It is possible to assign custom values. If the first value is assigned, the rest auto-increments.
enum Role {
Admin = "ADMIN",
User = "USER",
Guest = "GUEST",
}
let userRole: Role = Role.Admin;
console.log(userRole); // Output: "ADMIN"
String enums allow you to assign string values instead of numbers.
enum Sizes {
Small = 10,
Medium = Small * 2, // 20
Large = Medium * 2, // 40
}
console.log(Sizes.Large); // Output: 40
With computed enums you can use expressions to set values. The values auto-increment when not explicitly defined.
enum Status {
Active = "ACTIVE",
Inactive = 0,
Pending = 1,
}
console.log(Status.Active); // Output: "ACTIVE"
console.log(Status.Pending); // Output: 1
Enums can mix string and number values and form a heterogeneous enum, though it's not common.
enum Color {
Red = 1,
Green,
Blue,
}
console.log(Color.Green); // Output: 2
console.log(Color[2]); // Output: Green
With numeric enums, TypeScript supports reverse mapping, meaning you can access both the name and value. This allows accessing both value by name (Color.Green) and name by value (Color[2]).
const enum Size {
Small = "S",
Medium = "M",
Large = "L",
}
let tshirtSize: Size = Size.Medium;
console.log(tShirt); // Output: "M"
const enum provides a more optimized version that does not generate extra JavaScript code. This way no extra object is created in the compiled JavaScript, making the code more efficient.
function add(x: number, y: number): number {
return x + y;
}
console.log(add(5, 10)); // Output: 15
You can specify types for function parameters and return values. The parameters x and y must be numbers and the function must return a number.
function greet(name: string, message?: string): string {
return `Hello, ${name}! ${message || "How are you?"}`;
}
console.log(greet("Alice")); // Hello, Alice! How are you?
console.log(greet("Bob", "Welcome!")); // Hello, Bob! Welcome!
In TypeScript, function parameters are required by default. You can make parameters optional using ? or provide default values. The message? makes it optional, so if it's not provided, a default value can be used.
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4)); // Output: 10
console.log(sum(5, 10)); // Output: 15
Rest parameters allow a function to accept any number of arguments and treat them as an array. The ...numbers parameter allows passing multiple values.
function formatInput(input: number): number;
function format(input: string): string;
function format(input: string | number): string | number {
if (typeof input === "string") {
return input.toUpperCase();
} else {
return input * 2;
}
}
console.log(format("hello")); // Output: "HELLO"
console.log(format(10)); // Output: 20
console.log(format(true)); // Error: Argument of type 'boolean' is not assignable to 'string | number'
Function overloading allows you to define multiple function signatures for different argument types. TypeScript allows overloaded signatures to describe different ways a function can be called.
let multiply: (x: number, y: number) => number;
multiply = (a, b) => a * b;
console.log(multiply(4, 5)); // Output: 20
You can store functions in variables with explicit type definitions. This ensures the function always accepts two number arguments and returns a number.
interface MathOperation {
(x: number, y: number): number;
}
let multiply: MathFunction = (a, b) => a * b;
console.log(multiply(3, 4)); // Output: 12
Instead of using function types directly, you can define an interface. This makes the function signature reusable.
function logMessage(message: string): void {
console.log(message);
}
logMessage("Hello!"); // Works fine
If a function does not return a value, use void.
function throwError(message: string): never {
throw new Error(message);
}
The never type is used when a function never returns (e.g., throws an error or has an infinite loop).
type GreetFunction = (name: string) => string;
const sayHello: GreetFunction = (name) => `Hello, ${name}!`;
console.log(sayHello("Alice")); // Output: "Hello, Alice!"
You can define function types using the type keyword:
let value: any;
value = 10; // Number
value = "Hello"; // String
value = true; // Boolean
The any type disables type checking, allowing a variable to hold any value. Useful when migrating JavaScript code to TypeScript. While using this you lose type safety, which can lead to runtime errors.
let userInput: any;
userInput = 5;
userInput.toUpperCase(); // No error in TypeScript, but will fail at runtime!
Using any can be problematic, therefore it's a better alternative to use unknown instead of any if you need type safety.
let data: unknown;
data = "TypeScript";
if (typeof data === "string") {
console.log(data.toUpperCase()); // Safe operation
}
data.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'unknown'.
The unknown type is similar to any, but you must check the type before using it. Useful when dealing with dynamic values (e.g., user input, API responses). It forces type checking before usage, preventing runtime errors.
function logMessage(message: string): void {
console.log(message);
}
logMessage("Hello!"); // Works fine
A function that does not return a value should have a return type of void. Useful when a function performs an action but doesn't return anything.
function example(): undefined {
return undefined;
}
A function with return type undefined must explicitly return undefined.
function throwError(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {
console.log("Running forever...");
}
}
The never type represents functions that never complete. Used by functions that always throw errors and functions that never return (e.g., infinite loops, server listeners).
let someValue: unknown = "Hello, TypeScript!";
// Method 1: Using 'as' syntax (Recommended)
let strLength: number = (someValue as string).length;
// Method 2: Using angle-bracket syntax
let strLength2: number = (<string>someValue).length;
Type assertion is a way to manually override TypeScript's type inference. It does not change the runtime value, only the TypeScript type checking. The as syntax is preferred because angle brackets (<>) can cause issues in JSX/React.
function getUserInput(): unknown {
return "Hello World";
}
let input = getUserInput();
console.log(input.toUpperCase()); // TypeScript Error: Property 'toUpperCase' does not exist on type 'unknown'.
console.log((input as string).toUpperCase()); // Works fine
When receiving dynamic data (e.g., from an API), TypeScript treats it as unknown. To access properties safely, you must assert the type. Without type assertion, TypeScript won't allow accessing string methods on unknown.
let inputElement = document.getElementById("username") as HTMLInputElement;
inputElement.value = "TypeScript is awesome!";
When working with the DOM, TypeScript may return a HTMLElement | null. You can assert a more specific type (e.g., HTMLInputElement). In the above example we tell TypeScript that inputElement is an <input> and has a value property.
let myCanvas = document.getElementById("canvas")!;
We can also use ! (Non-null assertion operator) to tell TypeScript "this value will never be null".
function getFormattedValue(value: string | number) {
if ((value as string).toUpperCase) {
return (value as string).toUpperCase();
}
return value;
}
console.log(getFormattedValue("hello")); // Output: HELLO
console.log(getFormattedValue(42)); // Output: 42
If a variable has multiple possible types, TypeScript doesn't know which one to use. Type assertions can help narrow it down. In the above example TypeScript assumes value is string within the if block.
let num = "123" as number; // TypeScript Error! Can't convert string to number
let num: number = Number("123"); // Works fine
Type assertion only exists in TypeScript, not JavaScript. It does not change the actual value—just how TypeScript treats it.
let value: unknown = "Hello";
console.log((value as any) as number); // No error, but incorrect!
In some cases, TypeScript may not allow a type assertion. You can force it using any, but this is risky. This can lead to unexpected behavior at runtime.
let status: "success" | "error" | "pending";
status = "success"; // Valid
status = "error"; // Valid
status = "failed"; // Error: Type '"failed"' is not assignable to type '"success" | "error" | "pending"'.
With string literals, a variable can only hold specific string values. This is useful for API responses, configuration settings, or state management.
let diceRoll: 1 | 2 | 3 | 4 | 5 | 6;
diceRoll = 3; // Valid
diceRoll = 7; // Error: Type '7' is not assignable to type '1 | 2 | 3 | 4 | 5 | 6'.
You can restrict variables to specific numeric values. It's useful for limiting numeric values to predefined constants (e.g., dice rolls, user roles, response codes).
let isEnabled: true | false;
isEnabled = true; // Valid
isEnabled = false; // Valid
isEnabled = "yes"; // Error: Type '"yes"' is not assignable to type 'true | false'.
Boolean literals allow values to be strictly true or false. It helps enforce strict boolean checks in function parameters or conditions.
function setStatus(status: "success" | "error" | "pending") {
console.log(`Status set to: ${status}`);
}
setStatus("success"); // Works
setStatus("error"); // Works
setStatus("failed"); // Error: Argument of type '"failed"' is not assignable to parameter type.
Literal types can be used as function parameters to enforce specific values. It's useful in APIs where you want to restrict inputs to predefined values.
type Status = "success" | "error" | "pending";
let apiStatus: Status;
apiStatus = "success"; // Valid
apiStatus = "failed"; // Error
You can combine literal types into unions and use type aliases for better readability. Type aliases improve code readability and reusability.
type Shape =
{ kind: "circle"; radius: number }
| { kind: "square"; side: number };
function getArea(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius * shape.radius;
} else {
return shape.side * shape.side;
}
}
console.log(getArea({ kind: "circle", radius: 10 })); // Works
console.log(getArea({ kind: "triangle", base: 5, height: 10 })); // Error
Literal types are great for object-based type safety. It helps create type-safe discriminated unions for structured data.
const color = "red"; // Type is "red" (literal)
If you use const, TypeScript infers a literal type automatically.
let color = "red"; // Type is string, not "red"
const color = "red" as const; // Now color is strictly "red"
If you use let, TypeScript assumes the broader string type, which can be fixed by using as const to keep literals.
const config = {
theme: "dark",
language: "en"
} as const;
config.theme = "light"; // Error: Cannot assign to 'theme' because it is a read-only property.
Using as const makes an object's values immutable and inferred as literals. It ensures values in configurations, API responses, and enums are not accidentally changed.
type NewType<T> = {
[Key in keyof T]: TypeTransformation;
};
Mapped Types allow you to transform an existing type into a new type by iterating over its keys. In the above example keyof T gets all the keys of T, and [Key in keyof T] iterates over them.
type User = {
name: string;
age: number;
};
type ReadonlyUser = {
[K in keyof User]: Readonly<User[K]>;
};
// Equivalent to:
type ReadonlyUser2 = {
readonly name: string;
readonly age: number;
}
Let's take an object type and make all its properties readonly. All properties of User are now readonly in ReadonlyUser.
type ReadonlyUser = Readonly<User>; // Same as the mapped type above
TypeScript provides built-in utility types like Readonly<T> which can be used as a shortcut.
type OptionalUser = {
[K in keyof User]?: User[K];
};
// Equivalent to:
type OptionalUser2 = {
name?: string;
age?: number;
};
// Shortcut:
type OptionalUser3 = Partial<User>; // Same as above
This makes all properties optional.
type Mutable<T> = {
-readonly [K in keyof T]: T[K]; // `-readonly` removes readonly
};
type MutableUser = Mutable<ReadonlyUser>; // Now it's mutable
In this exampple we are creating a Mutable Type (Removing readonly)
type Fruit = "apple" | "banana" | "orange";
type FruitStock = {
[F in Fruit]: number;
};
// Equivalent to:
type FruitStock2 = {
apple: number;
banana: number;
orange: number;
};
This dynamically creates a type with keys "apple" | "banana" | "orange".
type Flexible<T> = {
readonly [K in keyof T]?: T[K]; // Adds `readonly` and `?`
};
type FlexibleUser = Flexible<User>;
// Equivalent to:
type FlexibleUser2 = {
readonly name?: string;
readonly age?: number;
};
You can mix and match readonly, ?, or even change the property type.
type RenameKeys
[K in keyof T as `new_${string & K}`]: T[K];
};
type NewUser = RenameKeys<User>;
// Equivalent to:
type NewUser2 = {
new_name: string;
new_age: number;
};
You can rename keys inside a mapped type. This renames each key by prefixing it with "new_".
T extends U ? TrueType : FalseType;
Conditional types allow you to dynamically determine a type based on a condition. If T extends U, return TrueType, otherwise return FalseType.
type IsString<T> = T extends string ? "Yes, it's a string" : "No, not a string";
type A = IsString<string>; // "Yes, it's a string"
type B = IsString<number>; // "No, not a string"
This checks if T is a string.
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type ExampleFunction = () => string;
type ExampleReturnType = GetReturnType<ExampleFunction>; // string
You can pass any type and apply conditions. Here, infer R extracts the return type from a function.
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null | undefined>; // string
This filters out null and undefined from a type.
type FilterNumbers<T> = T extends number ? T : never;
type Result = FilterNumbers<string | number | boolean>; // number
If T is a union, TypeScript distributes the conditional type. This removes string and boolean, keeping only number.
type Nullable<T> = {
[K in keyof T]: T[K] extends string | number ? T[K] | null : T[K];
};
type UserWithNullable = Nullable<User>;
// Equivalent to:
type UserWithNullable2 = {
name: string | null;
age: number | null;
};
You can combine both to create powerful types. The above example makes string and number properties nullable.
type User = {
name: string;
age: number;
};
type PartialUser = Partial<User>;
// Equivalent to:
type PartialUser2 = {
name?: string;
age?: number;
};
The Partial<T> utility makes all properties of a type optional.
type UserOptional = {
name?: string;
age?: number;
};
type RequiredUser = Required<UserOptional>;
// Equivalent to:
type RequiredUser2 = {
name: string;
age: number;
};
The Required<T> utility makes all properties required, even if they were optional. It ensures objects always have complete data.
type ReadonlyUser = Readonly<User>;
// Equivalent to:
type ReadonlyUser2 = {
readonly name: string;
readonly age: number;
};
const user: ReadonlyUser = { name: "John", age: 30 };
user.age = 31; // Error: Cannot assign to 'age' because it is a read-only property.
The Readonly<T> utility makes all properties immutable. It prevents accidental modifications to objects.
type UserBasicInfo = Pick<User, "name">;
// Equivalent to:
type UserBasicInfo2 = {
name: string;
};
function getUserInfo(user: Pick<User, "name">) {
console.log(`User name: ${user.name}`);
}
The Pick<T, K> utility extracts specific properties from a type. Useful when working with APIs or displaying only selected data.
type UserWithoutAge = Omit<User, "age">;
// Equivalent to:
type UserWithoutAge2 = {
name: string;
};
The Omit<T, K> utility removes specific properties from a type. It helps remove sensitive or unnecessary fields.
type Role = "admin" | "user" | "guest";
type Permissions = "read" | "write" | "delete";
type RolePermissions = Record<Role, Permissions[]>;
// Equivalent to:
type RolePermissions2 = {
admin: Permissions[];
user: Permissions[];
guest: Permissions[];
};
const permissions: RolePermissions = {
admin: ["read", "write", "delete"],
user: ["read"],
guest: ["read"],
};
The Record<K, T> utility creates an object type where the keys are K and values are T. It helps define object maps with predefined keys.
type Status = "success" | "error" | "pending";
type ExcludedStatus = Exclude<Status, "pending">;
// Equivalent to:
type ExcludedStatus2 = "success" | "error";
The Exclude<T, U> utility removes types from a union. It helps remove unwanted values from a union type.
type NullableString = string | null | undefined;
type NonNullString = NonNullable<NullableString>;
// Equivalent to:
type NonNullString2 = string;
The NonNullable<T> utility removes null and undefined from a type. It ensures values are always defined.
function getUser(): User {
return { name: "Alice", age: 25 };
}
type UserReturnType = ReturnType<typeof getUser>;
// Equivalent to:
type UserReturnType2 = {
name: string;
age: number;
};
The ReturnType<T> utility extracts the return type of a function. It helps when inferring return types dynamically.
function logMessage(message: string, count: number) {}
type LogParams = Parameters<typeof logMessage>;
// Equivalent to:
type LogParams2 = [string, number];
The Parameters<T> utility extracts the parameter types of a function as a tuple. It helps when reusing function parameter types.
function identity<T>(value: T): T {
return value;
}
console.log(identity<string>("Hello")); // "Hello"
console.log(identity<number>(42)); // 42
console.log(identity<boolean>(true)); // TrueType
console.log(identity("Auto Type")); // No need to specify <string>
Generics act as placeholders for types that will be determined later.
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair("Alice", 25); // result: [string, number]
You can use generics in functions to ensure type safety. Works with multiple generic types <T, U>.
interface Box<T> {
content: T;
}
const stringBox: Box<string> = { content: "Hello" };
const numberBox: Box<number> = { content: 100 };
You can use generics in interfaces for flexible structures. In the above example the Box<T> interface can hold any type.
class Storage<T> {
private items: T[] = [];
add(item: T) {
this.items.push(item);
}
getAll(): T[] {
return this.items;
}
}
const numberStorage = new Storage<number>();
numberStorage.add(42);
console.log(numberStorage.getAll()); // [42]
const stringStorage = new Storage<string>();
stringStorage.add("TypeScript");
console.log(stringStorage.getAll()); // ["TypeScript"]
Classes can also use generics. In this example the class works for any type, ensuring type safety.
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
console.log(getLength("Hello")); // Works (string has .length)
console.log(getLength([1, 2, 3])); // Works (array has .length)
console.log(getLength(42)); // Error: number doesn't have .length
Sometimes, you need to restrict the types that can be used. In this example T extends { length: number } ensures only types with a .length property can be used.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const user = { name: "Alice", age: 25 };
console.log(getProperty(user, "name")); // "Alice"
console.log(getProperty(user, "age")); // 25
console.log(getProperty(user, "email")); // Error: "email" is not a valid key
You can access object properties dynamically. In the example above K extends keyof T ensures only valid object keys can be accessed.
interface Response<T = string> {
data: T;
}
const success: Response = { data: "Success" }; // Defaults to string
const numbers: Response<number[]> = { data: [1, 2, 3] }; // Uses number[]
You can provide default types for generics. In this example the T = string sets string as the default type.
function swap<T, U>(a: T, b: U): [U, T] {
return [b, a];
}
console.log(swap("Hello", 42)); // [42, "Hello"]
Generics make utility functions more powerful. In this example the function swaps two different types.
interface ApiResponse<T> {
status: number;
data: T;
}
const userResponse: ApiResponse<{ name: string; age: number }> = {
status: 200,
data: { name: "Alice", age: 25 },
};
Generics work well in complex structures. It helps with type-safe API responses.
type Pair<T, U> = [T, U];
const example: Pair<string, number> = ["Alice", 25];
You can define generic types using type. It works similarly to generic interfaces.
// @ts-ignore
const value: number = "Hello"; // No TypeScript error
This tells TypeScript to ignore errors on the next line. Use sparingly, as it can hide genuine issues.
// @ts-nocheck
const x = "hello";
x.toFixed(2); // No error, even though x is a string
Place this at the top of a file to disable TypeScript type checking for the whole file. Useful for large third-party scripts or when migrating JavaScript to TypeScript.
// @ts-expect-error
const num: number = "Hello"; // This is expected to be an error
Unlike @ts-ignore, this will cause an error if TypeScript doesn't find an issue. Helps prevent unnecessary ignores.
const value: any = "Hello";
console.log((value as any).toFixed(2)); // No TypeScript error
const data: unknown = "Some string";
console.log((data as any).toFixed(2)); // Works, but risky
Casting to any disables type checking. Casting to unknown retains some safety.
The declare keyword in TypeScript is used to tell the compiler about the existence of variables, functions, classes, or modules that TypeScript doesn't have type information for. It helps when dealing with global variables, third-party JavaScript libraries, or ambient declarations without modifying existing code
// Declare a global variable that TypeScript doesn't know about
declare const API_BASE_URL: string;
console.log(API_BASE_URL); // No TypeScript error
When working with JavaScript code that defines a global variable but has no TypeScript definitions, you can use declare to inform TypeScript that the variable exists. declare does not generate actual JavaScript code; it just informs the TypeScript compiler.
// Assume this function exists in an external JS file but isn't defined in TypeScript
declare function fetchData(url: string): Promise
fetchData("https://api.example.com").then(console.log); // No TypeScript error
If you are using a JavaScript file with functions that TypeScript doesn't recognize, you can declare them.
// tax-lib.d.ts:
declare module "tax-lib" {
export function calculateTax(amount: number, rate: number): number;
}
// main.ts:
import { calculateTax } from "tax-lib";
const tax = calculateTax(100, 0.2);
console.log(tax); // 20
A TypeScript Declaration File (.d.ts) provides type definitions for existing JavaScript code. In this example a JavaScript library exposes a function called calculateTax, which has no TypeScript support, so we create a declaration file (tax-lib.d.ts) for it where we can provide our own types for the module's variables, functions, methods etc.
// Extend the global Window object
declare global {
interface Window {
appVersion: string;
}
}
// Now you can safely use window.appVersion
window.appVersion = "1.0.0";
console.log(window.appVersion);
If you need to add additional properties to an existing type (e.g., adding properties to Window in the browser), use declare.
{
"compilerOptions": {
"strict": false
}
}
You can disable strict type checking in tsconfig.json, but this is not recommended as it reduces TypeScript's benefits.
// Initialize a Node.js project
npm init -y
// Install TypeScript and required packages
npm install --save-dev typescript ts-node @types/node
// Initialize TypeScript - generate a tsconfig.json file
npx tsc --init
Setup TypeScript in a Node.js project
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"resolveJsonModule": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Modify the generated tsconfig.json for best TypeScript support.
// Install dotenv
npm install dotenv
// Create a .env file
PORT=5000
API_KEY=abcdef
// Create an src/config.ts files
import dotenv from "dotenv";
dotenv.config();
export const PORT = process.env.PORT || 3000;
export const API_KEY = process.env.API_KEY || "";
// Use in src/index.ts
import { PORT, API_KEY } from "./config";
console.log(`Server running on port ${PORT}`);
console.log(`API Key: ${API_KEY}`);
Handles environment variables in TypeScript. Avoids hardcoding secrets & makes configuration flexible. Make sure .env is in .gitignore to prevent leaks.
"scripts": {
"dev": "ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
To automate tasks, update package.json. You can run scripts:
// Install nodemon
npm install --save-dev nodemon
// Run script and restart if changes occur
nodemon src/index.ts
The script can be rerun if changes occur using nodemon.
This document's home with other cheat sheets you might be interested in:
https://gitlab.com/davidvarga/it-cheat-sheets
License:
GNU General Public License v3.0 or later