If you write JavaScript and don't use TypeScript yet, you're giving up the most effective tool to prevent production bugs. TypeScript is not a different language — it's JavaScript with static typing. The compiler warns you about errors before the code runs, not after. In this guide, I present everything you need to start using TypeScript in real projects.
Why TypeScript
JavaScript is dynamic — any variable can be anything at any time. This is flexible but dangerous. You only discover errors when the code runs and the user sees the error.
// JavaScript: this runs without error
function calculateTotal(price, quantity) {
return price * quantity
}
calculateTotal('10', 2) // Returns '102' (string), not 20
calculateTotal(10) // Returns NaN, no warningTypeScript catches these errors before running:
// TypeScript: the compiler warns you
function calculateTotal(price: number, quantity: number): number {
return price * quantity
}
calculateTotal('10', 2) // ✗ Error: Argument of type 'string' is not assignable to 'number'
calculateTotal(10) // ✗ Error: Expected 2 arguments, but got 1Basic types
TypeScript primitive types map directly to JavaScript types:
// Primitives
let name: string = 'Vinicius'
let age: number = 24
let isActive: boolean = true
// Arrays
let tags: string[] = ['react', 'next', 'typescript']
let scores: number[] = [95, 87, 92]
// Inline objects
let user: { name: string; email: string } = {
name: 'Vinicius',
email: 'vini@email.com'
}In practice, you almost never need to type variables explicitly — TypeScript infers the type automatically:
// Automatic inference — TS knows it's a string
let name = 'Vinicius' // type: string
let age = 24 // type: number
let tags = ['react'] // type: string[]
// Only type explicitly when necessary
let data: unknown = await fetchFromAPI()Interfaces and Types
For more complex objects, use interface or type. The practical difference is minimal — use interface for objects and type for unions and composite types:
// Interface — for objects
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
avatar?: string // optional field
}
// Type — for unions and compositions
type Status = 'pending' | 'confirmed' | 'failed' | 'refunded'
type ApiResponse<T> = {
data: T
success: boolean
error: string | null
}Interfaces can be extended, which is useful for type inheritance:
interface BaseEntity {
id: string
createdAt: Date
updatedAt: Date
}
interface User extends BaseEntity {
name: string
email: string
}
interface Order extends BaseEntity {
userId: string
total: number
status: Status
}
// User now has id, createdAt, updatedAt, name, emailTyped functions
Typing functions is where TypeScript shines the most. The compiler validates arguments and return type:
// Typed parameters and return
function createUser(name: string, email: string): User {
return {
id: crypto.randomUUID(),
name,
email,
role: 'user',
createdAt: new Date(),
updatedAt: new Date()
}
}
// Arrow function
const formatPrice = (cents: number): string => {
return `R$ ${(cents / 100).toFixed(2)}`
}
// Async function
async function getUser(id: string): Promise<User | null> {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) return null
return res.json()
}Generics: reusable types
Generics let you create functions and types that work with any type while keeping type safety. It's the most powerful TypeScript concept:
// Without generics — would need one function per type
function getFirstString(arr: string[]): string | undefined {
return arr[0]
}
function getFirstNumber(arr: number[]): number | undefined {
return arr[0]
}
// With generics — one function for all types
function getFirst<T>(arr: T[]): T | undefined {
return arr[0]
}
getFirst(['a', 'b', 'c']) // type: string | undefined
getFirst([1, 2, 3]) // type: number | undefined
getFirst<User>(users) // type: User | undefinedGenerics are essential for API functions, React hooks, and any reusable code:
// Generic API response
async function fetchAPI<T>(endpoint: string): Promise<ApiResponse<T>> {
const res = await fetch(`/api${endpoint}`)
const data = await res.json()
return data as ApiResponse<T>
}
// Usage — TS knows the return type
const { data: user } = await fetchAPI<User>('/users/123')
// user is of type User
const { data: orders } = await fetchAPI<Order[]>('/orders')
// orders is of type Order[]Utility Types: transforming types
TypeScript comes with utility types that transform existing types. These are the ones I use daily:
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
// Partial — all fields become optional
type UpdateUser = Partial<User>
// { id?: string; name?: string; email?: string; role?: ... }
// Pick — select specific fields
type UserPreview = Pick<User, 'id' | 'name'>
// { id: string; name: string }
// Omit — remove specific fields
type CreateUser = Omit<User, 'id'>
// { name: string; email: string; role: 'admin' | 'user' }
// Record — create typed object
type UserMap = Record<string, User>
// { [key: string]: User }The most common use is in forms and APIs — where you need variations of the same type:
// API: create user (no id, database generates it)
async function createUser(data: Omit<User, 'id'>): Promise<User> {
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
})
return res.json()
}
// API: partial update (all fields optional)
async function updateUser(id: string, data: Partial<User>): Promise<User> {
const res = await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(data)
})
return res.json()
}Enums vs Union Types
Enums exist in TypeScript but in practice, union types are simpler and don't generate extra JavaScript code:
// ❌ Enum — generates extra JS code, more complex
enum PaymentStatus {
PENDING = 'pending',
CONFIRMED = 'confirmed',
FAILED = 'failed',
}
// ✅ Union type — simpler, zero overhead
type PaymentStatus = 'pending' | 'confirmed' | 'failed'
// Both work the same for validation:
function processPayment(status: PaymentStatus) {
switch (status) {
case 'pending':
return 'Waiting...'
case 'confirmed':
return 'Confirmed!'
case 'failed':
return 'Failed.'
}
}Type guards: narrowing types
When a value can be of multiple types, type guards help TypeScript understand which type it is at each moment:
type Result = { success: true; data: User } | { success: false; error: string }
function handleResult(result: Result) {
if (result.success) {
// TS knows result.data exists here
console.log(result.data.name)
} else {
// TS knows result.error exists here
console.error(result.error)
}
}
// Custom type guard
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'email' in value
)
}
const data: unknown = await fetchSomething()
if (isUser(data)) {
// TS knows data is User here
console.log(data.email)
}Configuration: tsconfig.json
The tsconfig.json defines how TypeScript behaves in the project. For modern projects with Next.js or Node.js, this is the base I recommend:
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}The most important setting is "strict": true — it activates all type checks. Projects without strict mode lose most of TypeScript's value.
Patterns I use in production
After using TypeScript in every project, these are the patterns I apply most in my daily work:
- **Never use
any** — useunknownwhen you don't know the type and type guard to validate - Inference first — let TS infer the type whenever possible, only type explicitly when necessary
- Interfaces for component props — every React component prop should have an interface
- Utility types for APIs —
Omit<User, 'id'>for creation,Partial<User>for updates - Union types instead of enums — simpler, no runtime overhead
- **
as const** — for literal arrays and objects that shouldn't change
// as const — turns values into literal types
const ROLES = ['admin', 'user', 'moderator'] as const
type Role = typeof ROLES[number] // 'admin' | 'user' | 'moderator'
// React component props
interface ButtonProps {
label: string
variant?: 'primary' | 'secondary' | 'ghost'
onClick: () => void
disabled?: boolean
}
function Button({ label, variant = 'primary', onClick, disabled }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled} className={variant}>
{label}
</button>
)
}Conclusion
TypeScript is not about writing more code — it's about writing code the compiler can validate. Types document intent, interfaces define contracts between modules, and generics enable reuse without losing safety. If you come from JavaScript, the learning curve is smooth: start by typing functions and props, then advance to generics and utility types. The gain in productivity and code confidence shows up fast.
