TypeScript Data Types
TypeScript is a powerful superset of JavaScript that introduces static type checking to the dynamic nature of the web. By allowing developers to explicitly define the types of variables, function parameters, and return values, TypeScript catches potential bugs during the compilation phase rather than at runtime. This leads to more robust code, superior IDE autocompletion, and a significantly smoother refactoring experience.
The TypeScript type system is comprehensive, ranging from basic primitives to advanced constructs like generics and utility types.
At its core, TypeScript leverages the primitive types found in JavaScript. These serve as the building blocks for all other data structures.
let isDone: boolean = false;
let age: number = 30;
let username: string = "Alice";
let big: bigint = 100n;
let sym: symbol = Symbol("key");
let u: undefined = undefined;
let n: null = null;
Key considerations for primitives:
- number: Unlike languages that distinguish between integers and floats, TypeScript uses a single
numbertype for all numeric values. - bigint: Used for integers that exceed the safe limit of the
numbertype; these are denoted with annsuffix. - null and undefined: When the
strictNullCheckscompiler flag is enabled,nullandundefinedare treated as distinct types and cannot be assigned to other types without explicit handling. - symbol: Primarily used to create unique identifiers for object properties.
Arrays and Tuples
TypeScript provides flexible ways to handle collections of data.
Arrays are used when you have a list of elements of the same type. You can define them using the square bracket notation or the generic array type:
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
Tuples are a specialized version of arrays. They allow you to define an array with a fixed number of elements where each element has a specific type. This is ideal for representing structured data, such as coordinates or key-value pairs.
let tuple: [string, number] = ["age", 30];
tuple[0] = "score";
// tuple[1] = "30"; // Error: Type 'string' is not assignable to type 'number'.
Tuples can also be made more flexible using optional elements and rest elements:
let optionalTuple: [string, number?] = ["hello"];
let restTuple: [string, ...number[]] = ["nums", 1, 2, 3];
Enums
Enums (Enumerations) allow developers to define a set of named constants, making the code more readable and self-documenting.
Numeric Enums auto-increment their values starting from zero:
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
let dir: Direction = Direction.Up;
String Enums require explicit initialization for every member, which is often preferred for better debugging logs:
enum Status {
Success = "SUCCESS",
Fail = "FAIL",
}
For performance optimization, const enums can be used. These are completely removed during compilation, and their values are inlined in the JavaScript output, reducing the final bundle size.
const enum Color {
Red,
Green,
Blue,
}
Special Types
TypeScript introduces several special types to handle edge cases and complex logic.
- any: This effectively turns off type checking for a variable. While useful for migrating JS code, it should be avoided in production as it bypasses the benefits of TypeScript.
- unknown: A type-safe alternative to
any. You can assign anything tounknown, but you cannot perform any operations on it without first narrowing the type via a check. - void: Used primarily as the return type of functions that do not return a value.
- never: Represents values that will never occur. It is commonly used for functions that always throw an exception or enter an infinite loop.
- object: Represents any non-primitive type (objects, arrays, functions).
let value: unknown = "hello";
if (typeof value === "string") {
console.log(value.toUpperCase()); // Safe to use string methods here
}
function log(msg: string): void {
console.log(msg);
}
function fail(msg: string): never {
throw new Error(msg);
}
Union and Intersection Types
To represent more complex relationships between types, TypeScript offers Union and Intersection types.
Union Types (|) allow a value to be one of several types. This is particularly useful when a variable could legitimately hold different types of data.
let id: string | number;
id = 1;
id = "abc";
Intersection Types (&) combine multiple types into one. An object of an intersection type must satisfy all the requirements of the combined types.
type Point = { x: number; y: number };
type Named = { name: string };
type NamedPoint = Point & Named;
const p: NamedPoint = { x: 1, y: 2, name: "origin" };
Additionally, Literal Types can be used within unions to restrict a variable to a specific set of exact values:
type Move = "up" | "down" | "left" | "right";
let move: Move = "up";
// move = "jump"; // Error: Type '"jump"' is not assignable to type 'Move'.
Type Aliases and Interfaces
Both type and interface are used to define the shape of an object, but they serve slightly different purposes.
Type Aliases can define any type, including primitives, unions, and tuples.
type User = {
id: number;
name: string;
age?: number; // Optional property
readonly createdAt: Date; // Read-only property
};
Interfaces are specifically designed to describe object structures and support declaration merging (where multiple interface definitions with the same name are merged into one). They are also the standard for defining contracts in Object-Oriented Programming via inheritance.
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
department: string;
}
As a general rule: use interface for public API definitions and object structures, and use type for complex compositions like unions or aliases for primitives.
Type Assertions and Type Guards
Sometimes the developer knows more about a value's type than TypeScript does.
Type Assertions (as) tell the compiler to trust the developer's knowledge of the type.
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;
Type Guards are used to narrow down a type within a conditional block, ensuring type safety at runtime.
function isString(value: unknown): value is string {
return typeof value === "string";
}
function printId(id: string | number) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // id is narrowed to string
} else {
console.log(id.toFixed(2)); // id is narrowed to number
}
}
Generics and Utility Types
Generics allow you to create reusable components that work with a variety of types while maintaining type safety. They act as "type variables."
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("hello");
TypeScript also provides a set of Utility Types to transform existing types efficiently:
interface Todo {
title: string;
description: string;
completed: boolean;
}
// Makes all properties in Todo optional
type PartialTodo = Partial<Todo>;
// Creates a type by picking a set of properties from Todo
type TodoPreview = Pick<Todo, "title" | "completed">;
// Creates an object type with string keys and Todo values
type TodoMap = Record<string, Todo>;
Summary
TypeScript's type system transforms JavaScript from a flexible but error-prone language into a disciplined environment suitable for large-scale applications. By leveraging a mix of primitive types, interfaces, and generics, developers can create a "self-documenting" codebase where the types themselves explain how the data should flow.
To get the most out of TypeScript, follow these best practices:
- Minimize
any: Useunknownand type guards instead. - Prefer Interfaces for Objects: Use
interfacefor object shapes to take advantage of extensibility. - Embrace Generics: Use generics to avoid code duplication when handling multiple data types.
- Enable Strict Mode: Always keep
strictNullChecksenabled to eliminate the common "null pointer" errors.