Back to blog
Tutorial

TypeScript for JavaScript developers

By Flávio Emanuel · · 10 min read

Starting TypeScript when you come from vanilla JavaScript is like learning to drive with brakes. Feels slow at first. Then you realize the brakes save your life.

I’ve been developing for 8 years. 6 years pure JavaScript. Last 2 years TypeScript on everything. Not going back.

The difference is that TS finds errors before users find them. Kind of insurance for your code.

Why TS became standard

2026 and you can’t get a job without TypeScript. Why?

Because big companies know untyped code is a time bomb. Let it grow, two years later you hit an absurd bug nobody can find. Investing in types is investing in mental health.

Solo devs are also migrating. Why? Because when you have 5 clients with different projects, refactoring vanilla JavaScript is a nightmare. You change something and break everything without knowing.

TS forces you to think before you type. Seems slow at first, then saves weeks of debugging.

The 5 concepts that matter

I’ll simplify. TypeScript is big, but you don’t need to learn everything.

1. Basic types

let name: string = "John";
let age: number = 25;
let active: boolean = true;
let anything: any = "be careful with this";

Like this. You say what type a variable is. If you assign wrong, TypeScript complains before it runs.

Without types:

let user = "John";
user = user.toUpperCase(); // ok
user = 42; // now it's number, ok
user.toUpperCase(); // crash at runtime

With types:

let user: string = "John";
user = 42; // error in the editor already

Saves.

2. Interfaces

Interface is like a contract. Says what shape an object has.

interface User {
  name: string;
  age: number;
  email?: string; // optional
}

function create(user: User) {
  return `Creating ${user.name}`;
}

create({ name: "John", age: 25 }); // ok
create({ name: "John" }); // error, missing age
create({ name: "John", age: "25" }); // error, age not number

I use this on everything. Component props? Interface. API response? Interface. Function takes an object? Interface.

Saves the “wait, what’s the shape of this object again?” moment.

3. Generics

Generics are like a template. You define a function that works with any type, but keeps type safety.

function first<T>(list: T[]): T {
  return list[0];
}

const firstNumber = first([1, 2, 3]); // type: number
const firstString = first(["a", "b"]); // type: string

Like this. Function does the same thing, but type is inferred automatically.

I use it a lot with Supabase. When you query, return type is generic. TS figures out what type without you specifying.

4. Union types

Union is when a variable can be multiple types.

type Status = "active" | "inactive" | "pending";

function process(status: Status) {
  if (status === "active") {
    // do thing A
  } else if (status === "inactive") {
    // do thing B
  }
}

Seems simple. But prevents absurd errors like:

process("veryveryactive"); // error! only accepts active, inactive or pending

Very useful for UI states. Loading, error, success.

5. Type inference

TS can guess the type without you specifying always.

const name = "John"; // TS knows it's string
const numbers = [1, 2, 3]; // TS knows it's number[]

You don’t need to write const name: string = "John". TS figures it out.

This makes TypeScript less verbose than it seems. Day to day, you write types on interfaces and functions, and that’s it. TS handles the rest.

Setup with Astro

Astro comes with TS ready. Just create a .ts file instead of .js.

// src/api/appointments.ts
interface Appointment {
  id: string;
  patient: string;
  date: Date;
  duration: number; // minutes
}

export async function fetchAppointments(clinicId: string): Promise<Appointment[]> {
  const response = await fetch(`/api/appointments?clinicId=${clinicId}`);
  return response.json();
}

In an Astro component:

---
import { fetchAppointments } from '../api/appointments';

const appointments = await fetchAppointments("family-pilates");
---

<div>
  {appointments.map(apt => (
    <div>{apt.patient} - {apt.date.toLocaleDateString()}</div>
  ))}
</div>

TS validates appointments type automatically. If you try to access a property that doesn’t exist, editor complains.

Setup with React + Astro

If you’re using React Islands in Astro:

// src/components/AppointmentForm.tsx
interface AppointmentFormProps {
  clinicId: string;
  onSuccess?: (appointment: any) => void;
}

export default function AppointmentForm({ clinicId, onSuccess }: AppointmentFormProps) {
  const [loading, setLoading] = React.useState(false);

  async function schedule(data: FormData) {
    setLoading(true);
    // implementation
  }

  return <form onSubmit={schedule}>...</form>;
}

React + TS is better than React + vanilla JavaScript. Props are validated, state has type.

Common mistake: everything is any

New TS dev does this:

function process(data: any) {
  return data.whatever;
}

Then you gain nothing. any is like going back to vanilla JavaScript.

Better:

interface Data {
  whatever: string;
}

function process(data: Data) {
  return data.whatever;
}

Rule of thumb: if you write any, you haven’t understood the structure yet. Go back, understand it, come back without any.

Learning curve

First week is frustrating. You type something simple and TS complains about types.

Second week you start getting it. Third week you’re writing types before code.

Fourth week you can’t write vanilla JavaScript anymore.

With React vs Astro, I talked about when to use each. TS works on both.

If you’re starting a new project, use TS from day 1. Not overhead, it’s safety.

Old JavaScript project? Don’t need to refactor everything. Start writing new files in TS. Leave old JavaScript as is. Gradually migrate.

  • Install TS extension in editor (TypeScript Vue Plugin)
  • Create simple .ts file with basic types
  • Experiment with interface on real object
  • Use generic in reusable function
  • Add Union types for states
  • Configure tsconfig.json for the project
  • Migrate one complete component to TS

TypeScript is an investment that pays dividends every single day.

Discriminated union: smart typing

Advanced concept worth learning:

type ApiResponse =
  | { status: 'success'; data: string }
  | { status: 'error'; message: string }
  | { status: 'loading' }

function handleResponse(response: ApiResponse) {
  if (response.status === 'success') {
    console.log(response.data); // TS knows data exists here
  } else if (response.status === 'error') {
    console.log(response.message); // TS knows message exists here
  }
  // response.data here? TS complains, might not exist
}

TypeScript is smart: based on one field’s value, it knows other fields.

I use it a lot in Supabase queries where I differentiate success/error:

type SupabaseResult<T> =
  | { success: true; data: T }
  | { success: false; error: string }

async function fetchUser(id: string): Promise<SupabaseResult<User>> {
  const { data, error } = await supabase
    .from('users')
    .select('*')
    .eq('id', id)
    .single()

  if (error) {
    return { success: false, error: error.message }
  }

  return { success: true, data }
}

Then in code:

const result = await fetchUser('123')

if (result.success) {
  console.log(result.data.name) // TypeScript knows data exists
} else {
  console.log(result.error) // TypeScript knows error exists
}

Zero runtime overhead, complete safety.

Utility types: leverage the power

TypeScript has utility types that save lives:

interface User {
  id: string
  name: string
  email: string
  password: string // ← NEVER return this
}

// Omit: remove fields
type PublicUser = Omit<User, 'password'>

// Pick: take only some fields
type UserPreview = Pick<User, 'name' | 'email'>

// Partial: make everything optional
type UserUpdate = Partial<User>

// Record: create object with specific keys
type UserRole = Record<'admin' | 'user' | 'guest', boolean>

// Readonly: make everything immutable
type ReadonlyUser = Readonly<User>

I use this constantly:

export async function getPublicUser(id: string): Promise<PublicUser> {
  const user = await fetchUser(id)
  // Return 'password'? TS complains
  return { id: user.id, name: user.name, email: user.email }
}

TypeScript prevents you from accidentally exposing sensitive data.

Keyof: truly type-safe access

function getValue<T>(obj: T, key: keyof T) {
  return obj[key]
}

const user = { name: 'John', age: 25 }

getValue(user, 'name') // ok
getValue(user, 'phone') // error! doesn't exist

Truly type-safe. You can’t access properties that don’t exist.

Conditional types: advanced typing

Looks intimidating but powerful:

type IsString<T> = T extends string ? true : false

type A = IsString<'hello'> // true
type B = IsString<42> // false

Real use: API that returns different types based on input:

type ApiCall<T extends 'user' | 'post'> =
  T extends 'user' ? User : Post

function call<T extends 'user' | 'post'>(type: T): ApiCall<T> {
  // implementation
}

const user = call('user') // TypeScript knows it's User
const post = call('post') // TypeScript knows it's Post

TypeScript inferred type based on parameter. Magic.

Debugging types

Sometimes a type is weird. How to debug?

type Suspicious = SomeComplexType

// TypeScript will show what the type is
type Check = Suspicious

In editor, hover over Check and TypeScript shows the expanded type. Very useful to understand what’s happening.

Another tactic: use never to force error if not right:

type IsAny<T> = 0 extends (1 & T) ? true : false

const test: IsAny<any> = true // ok
const test2: IsAny<string> = true // error

Performance: TypeScript can slow builds

If you have very complex types, builds get slow.

Tactic: use as const when you know type won’t change:

// Without as const, TypeScript tries to be generic
const config = { port: 3000, host: 'localhost' } // type: { port: number, host: string }

// With as const, TypeScript is specific
const config = { port: 3000, host: 'localhost' } as const // type: { port: 3000, host: 'localhost' }

With as const, TypeScript doesn’t spend time generalizing. Build is faster.

Avoid any: rule number one

any is an escape hatch. Use only when you really can’t type:

// Reasonable
const data: any = JSON.parse(response)

// Bad
const result: any = await fetchData() // You should know the type

If you’re using any in a function you control, go back and type it right.

With Core Web Vitals I talked about performance. TypeScript matters too: well-typed code is fast code.

  • Install TypeScript and create first .ts file
  • Learn interfaces vs types
  • Practice generics with reusable function
  • Use discriminated unions in real project
  • Learn utility types (Pick, Omit, Partial)
  • Debug complex types with editor hover
  • Run TypeScript compiler and see errors
  • Migrate one component from .js to .ts

TypeScript at first is overhead. Later it’s security.

Next step

Need a dev who truly delivers?

Whether it's a one-time project, team reinforcement, or a long-term partnership. Let's talk.

Chat on WhatsApp

I reply within 2 hours during business hours.