LCM Logo
Programming

TypeScript Basics

Installing TypeScript

Install locally in a project (preferred):

shell
npm install --save-dev typescript

Or install globally:

shell
npm install -g typescript
tsc --version

Compiling TypeScript

shell
tsc app.ts   # produces app.js
node app.js

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true
  }
}

Run tsc with no arguments to compile using tsconfig.json.

Primitive Types

TypeScript's primitive types match JavaScript's: boolean, number, string, bigint, symbol, null, and undefined, plus the special types any, unknown, and never.

Type annotations are optional when TypeScript can infer the type:

typescript
// Explicit
let isDone: boolean = false;
let nDimensions: number = 3;
let name: string = "Cypress";

// Inferred (preferred)
let isDone = false;
let nDimensions = 3;
let name = "Cypress";

Constants

typescript
const nResamples = 10; // inferred as type 10 (literal type)
const nResamples: number = 10; // widened to number

Arrays

typescript
let scores: number[] = [1, 2, 3];

Enumerations

typescript
enum Color {
  Teal,
  Orange,
  Green,
  Blue,
}

let c: Color = Color.Orange;
console.log(Color[c]); // "Orange"

Numeric enums exist at runtime. const enum is fully erased:

typescript
const enum Direction {
  Up,
  Down,
}

Many teams prefer union types over enums — they are simpler and have no runtime footprint:

typescript
type Color = "Teal" | "Orange" | "Green" | "Blue";

Interfaces, Types, Classes, and Zod

These four constructs all describe the shape of data but serve different purposes.

interfacetypeclassZod schema
Runtime existenceNoNoYesYes
Runtime validationNoNoNoYes
Type inferenceIs the typeIs the typeIs the typez.infer<typeof S>
Instantiable (new)NoNoYesNo
Composabilityextends&, |extends.merge(), .extend(), .and(), .or()
Parsing / coercionNoNoNoYes

interface

Describes an object shape. Supports declaration merging — two declarations with the same name are merged:

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

interface User {
  email: string; // merged into the same interface
}

const user: User = { id: 1, name: "Alice", email: "alice@example.com" };

Extend with extends:

typescript
interface Admin extends User {
  role: string;
}

type

An alias for any type expression — objects, unions, intersections, tuples, mapped types:

typescript
type ID = string | number;

type Point = { x: number; y: number };

type Named = { name: string };
type NamedPoint = Point & Named; // intersection

Unlike interface, type cannot be re-opened for declaration merging.

class

Exists at runtime. Use when you need instantiation, methods, or inheritance:

typescript
class User {
  constructor(
    public id: number,
    public name: string
  ) {}

  greet() {
    return `Hello, ${this.name}`;
  }
}

const user = new User(1, "Alice");
user.greet(); // "Hello, Alice"

A class serves double duty: it defines both the runtime constructor and the TypeScript type of its instances.

Zod Schema

Zod is a runtime validation library. A Zod schema validates and parses external data (API responses, form input, env vars) and derives a TypeScript type:

typescript
import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>; // { id: number; name: string; email: string }

// parse throws on invalid data
const user = UserSchema.parse({ id: 1, name: "Alice", email: "alice@example.com" });

// safeParse returns { success, data } or { success, error }
const result = UserSchema.safeParse(unknownData);
if (result.success) {
  console.log(result.data.name);
}

Coercion converts string inputs (e.g. from query params):

typescript
const Schema = z.object({
  page: z.coerce.number(), // "3" → 3
});

When to use each

interface — for object shapes in application code, especially when you want to define a public API contract or expect declaration merging (e.g. module augmentation). The TypeScript team recommends interface over type for object shapes because it produces clearer error messages and supports merging.

type — for anything that isn't a plain object shape: unions, intersections, tuples, utility types, and aliases for primitives.

class — when you need instantiation, private state, methods, or instanceof checks. Not for plain data shapes.

Zod schema — whenever data crosses a trust boundary (HTTP responses, user input, environment variables, file parsing). Define the schema once; derive the type from it with z.infer.

A common pattern is to define the schema with Zod and use z.infer as the type throughout your application, avoiding the need for a separate interface or type:

typescript
// schema.ts
export const UserSchema = z.object({ id: z.number(), name: z.string() });
export type User = z.infer<typeof UserSchema>;

// api.ts
import { UserSchema, type User } from "./schema";
const user: User = UserSchema.parse(await response.json());

On this page