Getting Started with TypeScript Types
A beginner-friendly introduction to TypeScript's type system, covering primitives, interfaces, unions, type inference, and how to add types to existing JavaS...
Introduction
TypeScript is JavaScript with an optional type system layered on top. It compiles down to plain JavaScript, so it runs anywhere JavaScript runs, but along the way it catches an enormous number of bugs before your code ever executes — mismatched function arguments, typos in property names, and null values sneaking into places that expect real data. This guide introduces the core pieces of TypeScript's type system so you can start typing your own code with confidence.
Basic Types
TypeScript supports the same primitive values as JavaScript, just with explicit names you can attach to variables:
let username: string = "ada";
let age: number = 32;
let isAdmin: boolean = false;
let tags: string[] = ["engineer", "mentor"];
let coordinates: [number, number] = [12.9, 77.6]; // tuple
In most of these cases, you do not actually need the annotation, because TypeScript can infer the type from the initial value.
Type Inference
TypeScript is smart enough to figure out types on its own in the vast majority of cases:
let score = 95; // inferred as number
score = "ninety-five"; // Error: Type 'string' is not assignable to type 'number'
This is why experienced TypeScript developers rarely annotate local variables — inference already does the work. Annotations become important at the boundaries of your code: function parameters, return types, and exported values.
Typing Functions
function add(a: number, b: number): number {
return a + b;
}
function greet(name: string, greeting?: string): string {
return `${greeting ?? "Hello"}, ${name}!`;
}
The ? after greeting marks it as optional. Function parameters without a default value or ? are required, and TypeScript will flag any call site that omits them.
Object Shapes with Interfaces
An interface describes the shape an object must have:
interface User {
id: number;
name: string;
email: string;
isActive?: boolean;
}
function printUser(user: User) {
console.log(`${user.name} <${user.email}>`);
}
printUser({ id: 1, name: "Grace Hopper", email: "grace@example.com" });
If you pass an object missing email, or with an extra misspelled property, TypeScript reports the mismatch immediately, long before that bug would have surfaced at runtime.
Union and Literal Types
Union types let a value be one of several specific types, which is especially useful for modeling states:
type RequestState = "idle" | "loading" | "success" | "error";
function renderStatus(state: RequestState) {
if (state === "loading") return "Loading...";
if (state === "error") return "Something went wrong.";
return "Ready";
}
Because RequestState only allows those four exact strings, misspelling one anywhere in your codebase becomes a compile error instead of a silent bug.
Type Aliases and Combining Types
type lets you name any type, including unions, and can be combined with & to merge object shapes:
type Timestamped = { createdAt: string };
type User = { id: number; name: string };
type TimestampedUser = User & Timestamped;
const record: TimestampedUser = {
id: 1,
name: "Ada",
createdAt: "2024-01-01",
};
Working with Arrays and Objects Safely
TypeScript really shines when handling collections of data, since it tracks the type of every element:
interface Product {
id: number;
price: number;
}
function totalPrice(products: Product[]): number {
return products.reduce((sum, p) => sum + p.price, 0);
}
If you try to access products[0].pricee (a typo), TypeScript flags it instantly inside your editor.
unknown vs any
any opts a value out of type checking completely, which is risky. unknown is the safer alternative — it still requires you to narrow the type before you can use it:
function handleResponse(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase()); // safe, narrowed to string
}
}
Best Practices
- Let inference handle local variables; reserve explicit annotations for function signatures and exported values.
- Prefer
unknownoveranywhen a type genuinely cannot be known ahead of time. - Model finite sets of states with union of string literals instead of a generic
string. - Turn on
strictmode intsconfig.jsonfrom the start of a project — retrofitting strictness later is far more work. - Use interfaces for object shapes that might be extended, and type aliases for unions and utility types.
Common Mistakes to Avoid
- Reaching for
anythe moment TypeScript complains, which silences the exact safety net you added it for. - Typing every single variable explicitly, adding noise without adding safety.
- Forgetting that TypeScript types disappear at runtime — they cannot validate data coming from a network request or user input without an additional runtime check.
- Ignoring compiler errors instead of fixing the underlying type mismatch, which usually indicates a real bug.
Narrowing Types at Runtime
Static types disappear once your code compiles to JavaScript, but TypeScript can still track how a value's type changes as your code branches, a process called narrowing. Simple runtime checks like typeof, Array.isArray, or an instanceof check are enough for TypeScript to refine a broader type into a more specific one within that branch:
function formatValue(value: string | number | Date): string {
if (typeof value === "string") {
return value.toUpperCase(); // narrowed to string here
}
if (typeof value === "number") {
return value.toFixed(2); // narrowed to number here
}
return value.toISOString(); // by elimination, narrowed to Date
}
This is especially useful when working with union types or data of uncertain shape, such as an API response typed as unknown. Rather than casting with as (which bypasses checking entirely and just tells the compiler to trust you), writing an explicit narrowing check keeps the compiler actively verifying your assumptions:
function isUser(value: unknown): value is { id: number; name: string } {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
);
}
This kind of function is called a type predicate (note the value is ... return type), and calling it inside an if statement narrows value's type for the rest of that branch, giving you both a runtime check and compile-time safety from a single function.
Utility Types That Save You Boilerplate
TypeScript ships a set of built-in utility types that transform existing types instead of making you write near-duplicate interfaces by hand. Partial<T> makes every property optional, which is exactly what you want for an update function that only changes some fields:
interface User {
id: number;
name: string;
email: string;
}
function updateUser(id: number, changes: Partial<User>) {
// changes might only include { name: "New Name" }
}
Pick<T, K> and Omit<T, K> go the other direction, letting you derive a smaller type from a larger one by explicitly including or excluding keys — Pick<User, "id" | "name"> gives you just those two fields, while Omit<User, "email"> gives you everything except email. These are especially valuable for API layers, where a "create" request often omits the server-generated id, and a "public profile" view often omits sensitive fields like email. Reaching for a utility type instead of hand-writing a parallel interface keeps both types locked together — if you add a field to User, every derived type built with Partial, Pick, or Omit updates automatically, whereas a manually duplicated interface would silently drift out of sync.
Conclusion
TypeScript's type system is not about writing more code — it is about making the code you already write self-documenting and safer to change. Start small: add interfaces to your main data models, type your function signatures, and let inference do the rest. Within a few files, you will start noticing bugs that TypeScript catches before you even run your program.
Article FAQ
References
Comments
Comments are coming soon. Meanwhile, share your feedback via our contact page.
Related Articles
View allTypeScript Generics: A Practical Guide
Learn TypeScript generics from the ground up with practical examples covering generic functions, constraints, default types, and generic React-style utilities.
Top JavaScript Interview Questions and Answers
A curated list of common JavaScript interview questions with clear explanations, covering closures, hoisting, the event loop, prototypes, and equality compar...
React Hooks: A Complete Guide for Beginners
A beginner-friendly tour of React's built-in hooks, including useState, useEffect, useContext, useRef, and useMemo, with practical examples for each one.
The JavaScript Event Loop Explained
Understand how the JavaScript event loop works, including the call stack, task queue, and microtask queue, with clear diagrams-in-text and runnable examples.