TypeScript for JavaScript Developers 2026: From Types to a Typed REST API

TypeScript for JavaScript Developers 2026: From Types to a Typed REST API

If you already know JavaScript ES6+ and have heard the TypeScript hype for years, 2026 is the year to finally make the jump. This typescript tutorial walks you through everything a working JavaScript developer needs: core type annotations, interfaces vs type aliases, generics, a full tsconfig.json setup with strict mode, and a typed Express REST API you can copy and run today.


1. Why TypeScript in 2026?

TypeScript has moved from "nice to have" to industry standard:

  • 30 million weekly npm downloads for the typescript package — more than React.
  • 43.6% of Stack Overflow respondents use TypeScript, making it the most-loved compile-to-JS language for the fifth year running.
  • Node.js 22 runs .ts files natively with the --experimental-strip-types flag — no build step required for many dev workflows.

The core value proposition is unchanged: catch bugs at edit time rather than at runtime, get first-class IDE autocomplete on every variable and parameter, and make large codebases refactorable with confidence. What has changed is the friction — it is now close to zero.


2. Install TypeScript and Set Up tsconfig.json with Strict Mode

Start a fresh project:

mkdir ts-rest-api && cd ts-rest-api
npm init -y
npm install typescript tsx express
npm install --save-dev @types/express @types/node
npx tsc --init

npx tsc --init generates a default tsconfig.json. Replace its contents with this production-ready baseline for a Node.js project:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

The single most important option is "strict": true. This master switch enables seven safety flags at once: strictNullChecks, strictFunctionTypes, noImplicitAny, strictPropertyInitialization, strictBindCallApply, noImplicitThis, and useUnknownInCatchVariables. Enable it from day one — retrofitting it onto an existing codebase is painful.


3. Core Types: Primitives, Arrays, Objects, and Union Types

TypeScript adds a type annotation after a colon. Create src/types-demo.ts:

// Primitives — map directly to JavaScript's runtime types
const username: string = "alice";
const score: number = 42;
const isAdmin: boolean = false;

// Arrays — two equivalent syntaxes
const tags: string[] = ["typescript", "nodejs"];
const scores: Array<number> = [10, 20, 30];

// Objects — inline shape annotation
const user: { id: number; name: string } = { id: 1, name: "Alice" };

// Union types — the variable can hold more than one type
let id: string | number = "abc-123";
id = 456; // also valid

// Literal types — restrict to a fixed set of values
type Status = "active" | "inactive" | "pending";
const accountStatus: Status = "active";

TypeScript infers types from assignments whenever it can, so you do not need to annotate every variable. Annotate function signatures and exported values; let inference handle local variables.


4. Interfaces vs Type Aliases — When to Use Each

Both interface and type can describe the shape of an object. The practical rule: use interface for objects and classes, use type for everything else (unions, intersections, function signatures, computed shapes).

// Interface — extendable, supports declaration merging
interface User {
  id: number;
  name: string;
  email: string;
}

// Extending an interface
interface AdminUser extends User {
  role: "admin";
  permissions: string[];
}

// Type alias — best for unions, intersections, and function types
type ID = string | number;
type Nullable<T> = T | null;
type UserOrAdmin = User | AdminUser;

// Type aliases can express things interfaces cannot
type ApiHandler = (req: unknown, res: unknown) => Promise<void>;

The key difference: interfaces support declaration merging — two interface User blocks in the same scope silently merge into one. This is useful when augmenting third-party library types. Type aliases do not merge; redefining a type is a compile error.


5. Functions: Typed Parameters, Return Types, and Optional Parameters

Typed functions are where TypeScript pays back immediately — you get autocomplete on every parameter and a compile-time error when a caller passes the wrong shape.

// Explicit parameter and return types
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// Optional parameter with ?
function createUser(name: string, role?: string): User {
  return {
    id: Date.now(),
    name,
    email: `${name.toLowerCase()}@example.com`,
  };
}

// Default parameter
function paginate(page: number = 1, limit: number = 20): string {
  return `Page ${page}, ${limit} items`;
}

// Arrow function with inline type annotation
const double = (n: number): number => n * 2;

// Function that may return null — caller must handle both branches
function findUserById(id: number, users: User[]): User | null {
  return users.find((u) => u.id === id) ?? null;
}

With strictNullChecks enabled (part of strict), TypeScript forces every caller to handle the null case before accessing properties — eliminating a whole class of runtime Cannot read properties of null crashes.


6. Generics: Write Once, Type Correctly for Any Type

Generics let you write a function or interface once and have it work correctly for any type — without losing type information. The classic example is the identity function:

function identity<T>(x: T): T {
  return x;
}

const n = identity(42);      // TypeScript infers T = number; n is number
const s = identity("hello"); // TypeScript infers T = string; s is string

Practical Generic Examples

// Return the first element of any typed array
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const firstUser = first<User>([{ id: 1, name: "Alice", email: "[email protected]" }]);
// firstUser is typed as User | undefined

// Constrain T to shapes that have an id property
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find((item) => item.id === id);
}

// Generic API response wrapper — use this across every endpoint
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

function ok<T>(data: T): ApiResponse<T> {
  return { data, status: 200, message: "OK" };
}

const userResponse = ok<User>({ id: 1, name: "Alice", email: "[email protected]" });
// userResponse.data is typed as User

Generics are the backbone of TypeScript's standard library. Once you understand Array<T>, Promise<T>, and Record<K, V>, the rest of the ecosystem clicks into place.


7. Build a Typed Express REST API

Create src/server.ts. This is a complete, fully typed Express server with two resource endpoints.

import express, { Request, Response } from "express";

const app = express();
app.use(express.json());

// ---- Domain types ----

interface User {
  id: number;
  name: string;
  email: string;
}

interface CreateUserBody {
  name: string;
  email: string;
}

// ---- In-memory store ----

const users: User[] = [
  { id: 1, name: "Alice", email: "[email protected]" },
  { id: 2, name: "Bob",   email: "[email protected]"   },
];

// ---- Routes ----

// GET /users — return all users
app.get("/users", (_req: Request, res: Response<User[]>) => {
  res.json(users);
});

// GET /users/:id — return a single user or 404
app.get(
  "/users/:id",
  (req: Request<{ id: string }>, res: Response<User | { error: string }>) => {
    const user = users.find((u) => u.id === Number(req.params.id));
    if (!user) {
      res.status(404).json({ error: "User not found" });
      return;
    }
    res.json(user);
  }
);

// POST /users — create a new user with a typed request body
app.post(
  "/users",
  (req: Request<{}, {}, CreateUserBody>, res: Response<User>) => {
    const { name, email } = req.body;
    const newUser: User = { id: users.length + 1, name, email };
    users.push(newUser);
    res.status(201).json(newUser);
  }
);

// ---- Start server ----

const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));

The key is Request<Params, ResBody, ReqBody>. Providing CreateUserBody as the third generic argument means req.body.name and req.body.email are fully typed — no any, no manual casts, and a compile error if you mistype a field name.

Test the endpoints:

# Get all users
curl http://localhost:3000/users

# Create a user
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Carol","email":"[email protected]"}'

8. Running with tsx (Dev) and tsc (Production Build)

Developmenttsx transpiles TypeScript on the fly with no configuration:

npx tsx src/server.ts

Add scripts to package.json:

{
  "scripts": {
    "dev":   "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}
npm run dev    # watch mode — restarts on file changes
npm run build  # compile to dist/
npm start      # run compiled output

Node.js 22 native option — for scripts and CLIs you can skip the build step entirely:

node --experimental-strip-types src/server.ts

This strips type annotations at runtime with no separate compilation. It does not support every TypeScript feature (no decorators, no const enum), but it is zero-config for simple scripts and tools.

Production workflow — always compile with tsc before deploying. The compiled .js files in dist/ run on any Node version and do not require TypeScript at runtime:

npm run build && npm start

9. FAQ: Common TypeScript Errors Explained

Type 'string' is not assignable to type 'number' You passed or assigned a string where a number is expected. Check the annotation on the variable or parameter and fix the value type at the call site.

Object is possibly 'null' With strictNullChecks on, TypeScript requires you to guard against null before accessing a property. Use an if check (if (user) { ... }), the nullish coalescing operator (user ?? defaultUser), or optional chaining (user?.name).

Property 'x' does not exist on type 'Y' You're accessing a property that isn't declared in the type. Either add the property to the interface or, if it comes from an external source, use a type assertion — but prefer expanding the interface.

Parameter 'x' implicitly has an 'any' type With noImplicitAny (part of strict), every function parameter needs a type annotation. Add : string, : number, or whatever fits. If the type is genuinely unknown, use : unknown and narrow before use.

Cannot find module '...' or its corresponding type declarations You imported a JavaScript package that ships no bundled types. Install the types package: npm install --save-dev @types/<package-name>. If no @types package exists, add a minimal ambient declaration in src/global.d.ts:

declare module "some-untyped-package";

Type 'X' is not assignable to type 'Y' on a function return Your function declares a return type but one of its code paths returns something different. Fix the return value, or if the type is genuinely a union, update the return type annotation to reflect that.


You now have a working tsconfig.json typescript tutorial setup, understand the difference between interfaces and type aliases, can write generic functions with constraints, and have a running typed Express REST API. The natural next steps are TypeScript's utility types (Partial<T>, Readonly<T>, Pick<T, K>, Omit<T, K>), discriminated unions for modelling state machines, and frameworks like NestJS that treat TypeScript as a first-class citizen.

Leonardo Lazzaro

Software engineer and technical writer. 10+ years experience in DevOps, Python, and Linux systems.

More articles by Leonardo Lazzaro