This quarter, one of my company goals was to dive deeper into TypeScript using TypeHero challenges. I want to share my learnings about some of TypeScript's most powerful features. No matter where you are in your coding journey—whether you're a seasoned pro or a beginner—this guide will provide you with the foundational knowledge to confidently dive into TypeScript.
So, let's get typing!
Table of Contents
- Primitive Data Types
- Type Aliases
- Literal Types
- Type Unions
- Generic Function Arguments
- Generic Type Arguments
- Default Generic Arguments
- Generic Type Constraints
- Index Signatures
- Indexed Types
- The keyof Operator
- The typeof Operator
- Mapped Object Types
Primitive Data Types
TypeScript includes all JavaScript primitive types: string
, number
, boolean
, null
, undefined
, symbol
, and bigint
. These form the foundation of the type system:
let name: string = "Alice";
let age: number = 25;
let isActive: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
let uniqueSymbol: symbol = Symbol("id");
let bigNumber: bigint = 9007199254740991n;
Type Aliases
Type aliases let you create custom names for types. They're great for reusability and making your code more readable:
type UserID = string;
type Point = {
x: number;
y: number;
};
// Using the types
const userId: UserID = "user123";
const coordinate: Point = { x: 10, y: 20 };
Literal Types
Literal types allow you to specify exact values that a type can have:
type Direction = "north" | "south" | "east" | "west";
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
function move(direction: Direction, spaces: DiceRoll) {
console.log(`Moving ${spaces} spaces ${direction}`);
}
move("north", 3); // Valid
// move("northeast", 7); // Error: Invalid direction and number
Type Unions
Unions allow a value to be one of several types. They're perfect for handling multiple possible types:
type StringOrNumber = string | number;
type Status = "loading" | "success" | "error";
function processId(id: StringOrNumber) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(2));
}
}
Generic Function Arguments
Generics make functions more flexible by allowing them to work with different types while maintaining type safety:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
// TypeScript infers the correct return types
const numberResult = firstElement([1, 2, 3]); // type: number
const stringResult = firstElement(["a", "b", "c"]); // type: string
Think of them as placeholders for different data types within a function.
Generic Type Arguments
Generic type arguments work similarly but for type definitions:
type Box<T> = {
content: T;
timestamp: Date;
};
const stringBox: Box<string> = {
content: "Hello",
timestamp: new Date()
};
const numberBox: Box<number> = {
content: 42,
timestamp: new Date()
};
Like function arguments, but for defining flexible data structures.
Default Generic Arguments
You can specify default types for generics, similar to default function parameters:
interface ApiResponse<T = any> {
data: T;
status: number;
message: string;
}
// Uses the default 'any' type
const simpleResponse: ApiResponse = {
data: "anything",
status: 200,
message: "Success"
};
// Specifies a custom type
const typedResponse: ApiResponse<string[]> = {
data: ["item1", "item2"],
status: 200,
message: "Success"
};
It provides a fallback type if none is explicitly specified.
Generic Type Constraints
Constraints limit what types can be used with generics using the extends
keyword:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength("hello"); // Works: strings have length
logLength([1, 2, 3]); // Works: arrays have length
// logLength(123); // Error: numbers don't have length
Ensure that only specific types can be used in a generic context.
Index Signatures
Index signatures allow you to type objects with dynamic property names:
type Dictionary = {
[key: string]: string;
};
const colors: Dictionary = {
red: "#FF0000",
green: "#00FF00",
blue: "#0000FF"
};
// Any string key is allowed
colors.purple = "#800080";
Define the shape of objects with a variable number of properties.
Indexed Types
Indexed types let you access the type of a property in another type:
interface User {
name: string;
age: number;
email: string;
}
type AgeType = User["age"]; // type: number
type ContactInfo = User["email" | "name"]; // type: string
Access and manipulate the types of specific properties within an object.
The keyof Operator
keyof
gets all property names from a type as a union:
interface Product {
id: number;
name: string;
price: number;
}
type ProductKeys = keyof Product; // "id" | "name" | "price"
function getProperty(obj: Product, key: ProductKeys) {
return obj[key];
}
Extract all possible property names from a given type.
The typeof Operator
In type contexts, typeof
creates a type based on the type of a value:
const user = {
name: "Alice",
age: 25,
role: "admin"
};
type User = typeof user;
// Equivalent to:
// type User = {
// name: string;
// age: number;
// role: string;
// }
Determine the type of a variable or expression at compile time.
Mapped Object Types
Mapped types let you create new types based on old ones by transforming properties:
type Optional<T> = {
[K in keyof T]?: T[K];
};
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
interface Todo {
title: "string;"
description: "string;"
}
type OptionalTodo = Optional<Todo>;
// Equivalent to:
// {
// title?: string;
// description?: string;
// }
Transform existing object types into new ones by modifying their properties.
Conclusion
TypeScript's type system is both powerful and flexible, making it an essential tool for developers. While it may feel overwhelming at first, consistent practice and exploration will unlock its true potential. Dive into resources like TypeHero, experiment with these concepts in your projects, and explore the TypeScript documentation to deepen your understanding. The best way to master TypeScript is to start applying it—one step at a time!
Happy coding!
Top comments (0)