Understanding TypeScript

A Beginner-Friendly Dive Into Types

If you have worked in a JavaScript frontend or backend application, you have at least heard of TypeScript. And by heard, I mean heard something bad. If not, you’re in luck. However, it is a pretty common experience.

Yet, there is much more to TypeScript than meets the eye at first glance. It can help you code in a better, more predictable way. But, before diving into what it is and what it can do for us, let’s focus on: types.

Types and Type System

In the world of programming, types play a crucial role in defining the kind of data that a variable can hold. Whether you’re working with numbers, strings, or more complex objects, understanding types is foundational to writing reliable and maintainable code.

A type system is a set of rules that assigns *labels (*types) to values and variables in a program. These labels describe what kind of data you’re working with and what you can (or can’t) do with it.

Typing is the practice of using these types to define the shape and behavior of values in your code. It answers questions like: “What kind of data does this variable hold?” and “What operations are allowed on it?”.

let message: string = "Hello, world!";

Here, string is the type. It tells the compiler that message is expected to hold a text value.

Now, going further, a type doesn’t just describe what a value is: it also defines how you can interact with it. These interactions are called operations.

  • Calling methods (e.g., .map() on an array, .toFixed() on a number)
  • Performing arithmetic (e.g., +, on numbers)
  • Accessing properties (e.g., user.name in an object)
  • Passing values to functions expecting a specific type of those values

Let’s recheck our message variable: since the value will be a text, you can safely use string operations like .toUpperCase(), but trying to use something like .push() (which only makes sense for arrays) would result in an error.

This way, types allow you to write safer, more predictable code by enforcing the rules of how data should behave.

Typescript: what is it?

Now that we are clear on types, let’s go back to where we’ve started: What is TypeScript?

TypeScript is a superset of JavaScript that allows you to define the types of your variables, ensuring that they are consistent throughout your code. By catching type-related errors at compile time, TypeScript helps developers avoid common pitfalls and creates a more predictable development experience.

TypeScript is a superset of JavaScript
TypeScript is a superset of JavaScript

Static Analysis

In TypeScript, types are static, which means they are known and checked at compile time. This allows for static analysis, where potential errors can be caught early, before the code even runs. In contrast, languages like JavaScript and Ruby use dynamic typing, where types are determined at runtime, making it harder to catch certain mistakes until the code is actually executed. With static types, the structure of your code can be analyzed to ensure types are being used consistently. This prevents bugs from making it to production and gives you faster feedback as you write code.

let age: number = 25;
age = "twenty-five";  // Error: Type 'string' is not assignable to type 'number'

Build-time-only

TypeScript is only used during development, and, after that, it does not exist. Once your TypeScript code is written, it is compiled into plain JavaScript, and this JavaScript is what actually runs in the browser or Node.js.

Now that we understand what it is, let’s talk about the key points of TypeScript and what it can do for us.

Key points of Typescript

Let’s check some of the key characteristics that make TypeScript a robust tool:

  • Type safety for JS classes

Beyond the primitive types, TS will also type all JS classes, bringing type safety to complex structures, such as Array, Date, and others.

const now: Date = new Date();

TypeScript understands the types behind these built-ins and will guide you with autocompletion and error checking. You don’t need to define types for built-in objects — they come with TypeScript’s type definitions.

  • Types as a new dimension

In JS, you define and manipulate values at runtime. TS will add a second dimension to that: the types, existing at compile time. While values tell you what the program does, the types describe what the data should be.

This duality allows you to:

  • Think more clearly about the shape and intent of your code

  • Catch structural mistakes before running anything

  • Work with your editor like a real-time assistant

  • Explicit and Inferred Typing

When typing your code, TS will support two ways of doing it:

  1. Explicit Typing: You declare it yourself, as we saw before with our message variable.
let message: string = "Hello, world!";
  1. Type inference: TS will figure out by itself
let message = "Hello, world!";  // Inferred as string

In most cases, inference works well and keeps your code clean. But in function signatures, shared types, and APIs, explicit typing makes your intent clear and avoids ambiguity. Let’s talk some more about explicit types.

Defining and composing types

In TypeScript, you can create your own types and even compose them together to describe more complex data. This isn’t just about listing properties on an object — it’s about modeling the logic and relationships of your program in a way that’s both human-readable and machine-checkable.

Defining types

First of all, let’s assign types to your data. Let’s start with simple values:

let name: string = "Mary";
let age: number = 28;
let isLoggedIn: boolean = true;

These are basic type annotations — useful for clarity and for catching accidental assignments.

However, you can think a little further and add types or interfaces to your objects. Here we will focus on types, but we can learn more about the differences in this hands-on here.

Let’s say you have a User object, which will always have the properties name, email, and id. If you type it, you will guarantee a consistent reuse of this object across the entire application.

type User = {
  id: number;
  name: string;
  email: string;
};

const currentUser: User = {
  id: 1,
  name: "Mary",
  email: "marya@example.com",
};

Another place in your code where you will define types will be in your functions. You will type both the function’s parameters and its return:

function greet(user: User): string {
  return `Hello, ${user.name}`;
}

This way, the function only accepts User as a parameter and will always return a string, which ensures safety and readability. Any changes to this function, without changes in types, will show you an error, such as:

function greet(user: User): string {
  return `Hello, ${user.username}`; // ❌ Error!
}

TS Error:
Property 'username' does not exist on type 'User'. Did you mean 'name'?

In this case, you are trying to access a property that is not available, according to your Usertype.

Typing API Data

Now that we’ve covered these type definitions, let’s use them to type API data. The components in your application are often powered by data fetched from APIs. This data can be user profiles, product listings, blog posts, or anything else, and it’s usually retrieved in JSON format.

In JavaScript, you’d get this data and hope it’s structured correctly. However, in TypeScript, you can define the expected structure, so the compiler can validate that your code matches the data contract.

Let’s see an example. Let’s fetch blog posts from an API. According to the API’s documentation, we have the following data structure:

// GET /api/posts
[
  {
    "id": 1,
    "title": "Introduction to TypeScript",
    "author": "Julia",
    "publishedAt": "2024-05-01T12:00:00Z"
  }
]

Based on that, we can define the type:

type Post = {
  id: number;
  title: string;
  author: string;
  publishedAt: string; // could also be Date if you convert it
};

And use it to define the return of your function responsible for fetching this data:

async function fetchPosts(): Promise<Post[]> {
  const response = await fetch("/api/posts");
  const data = await response.json();
  return data;
}

Your fetchPosts function now returns a Promise of an array of Post objects. This helps your editor and TypeScript tooling understand exactly what you’re working on within every part of your app.

And now you can use this data in your component safely:

function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title} by {post.author}</li>
      ))}
    </ul>
  );
}

In case you make a mistake, such as trying to access post.content (which doesn’t exist), TypeScript will immediately warn you.

Composing types

Now that you have defined your basic types, you can combine them to create richer, more flexible structures. This is called type composition.

It allows you to describe real-world logic like: “This thing can be either this or that” (union), or “This thing must be both this and that” (intersection).

Union Types

Let’s go back to our Post example. Let’s say we have a few statuses for our post, such as “draft”, “pending review”, and “published”. A PostStatus type must include all the options, and that’s where union types come in handy: a union type says, “This value can be one of several types.”

type Status = "draft" | "pending review" | "published";

let postStatus: Status = "published";

Intersection Types

An intersection type allows you to combine multiple types into one. This is useful when you want to add extra info to an existing type.

Let’s say you want to include metadata in your posts, like tags and view count. You can create a new type for the metadata:

type Metadata = {
  tags: string[];
  views: number;
};

And then combine it with our original Post type:

type PostWithMetadata = Post & Metadata;

And now the combined type PostWithMetadata includes everything from both types:

const postWithMetadata: PostWithMetadata = {
  id: 1,
  title: "Understanding Types",
  author: "Julia",
  publishedAt: "2025-06-13",
  tags: ["typescript", "frontend"],
  views: 1200,
};

By composing from the original Post type, you reduce repetition and centralize shared structure, also add flexibility through unions and intersections, and can scale safely as your app grows (new features = new layers).

Your Post type acts like a core contract, and all variations grow from it in a controlled, predictable way.

What TypeScript is not

So far we’ve talked about what TS can do for you, but let’s be clear about what it can’t do because misconceptions might lead to confusion or disappointment.

  • TS doesn’t optimize JS performance: It doesn’t optimize your code, compress it, or make it run more efficiently in the browser. What TypeScript does is help you write more correct code before it runs. But once it’s compiled, the final output is just plain JavaScript.
  • TS doesn’t guarantee runtime type safety: as we’ve discussed before, it performs static analysis, which means it will check your code while you write it. If your API returns unexpected data, someone mutates an object incorrectly at runtime or a bug passes along all your type assumptions (among other issues), TS will not catch any of that. It can only check the types you say you’re working with, not what the actual values are when the app is running. There are validation tools for that.
  • TS doesn’t replace testing: it will catch bugs early (typos, invalid function usage, and so on), but it cannot check what your app actually does. Keep those tests going!

And, above all, TypeScript is not just a bunch of pointless extra code, adding noise to your codebase. Type annotations will improve autocomplete and navigation, help to document your code in place, help developers refactor with confidence, and catch many bugs before you even run your app.

Conclusion

In this post, we saw how TypeScript works, what types really are, how to define and combine them, and how it fits into a real-world frontend app, like a blog. We also talked about what TypeScript doesn’t do, like improving runtime performance or replacing tests.

At the end of the day, TypeScript is a tool to help you think more clearly and make your code more reliable. It might feel like extra work at first, but as your project grows, it quickly becomes worth it. It doesn’t make your app better on its own, but it helps you write better apps.

References

Documentation – TypeScript for JavaScript Programmers
Oh no, I have to add those stupid TypeScript types
React TypeScript Cheatsheets
TS Playground

Previously: What you need to know about frontend design patterns

This post is part of our ‘The Miners’ Guide to Code Crafting’ series, designed to help aspiring developers learn and grow. Stay tuned for more!

We want to work with you. Check out our Services page!