TypeScript Basics
Installing TypeScript
Install locally in a project (preferred):
npm install --save-dev typescriptOr install globally:
npm install -g typescript
tsc --versionCompiling TypeScript
tsc app.ts # produces app.js
node app.jstsconfig.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:
// 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
const nResamples = 10; // inferred as type 10 (literal type)
const nResamples: number = 10; // widened to numberArrays
let scores: number[] = [1, 2, 3];Enumerations
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:
const enum Direction {
Up,
Down,
}Many teams prefer union types over enums — they are simpler and have no runtime footprint:
type Color = "Teal" | "Orange" | "Green" | "Blue";Interfaces, Types, Classes, and Zod
These four constructs all describe the shape of data but serve different purposes.
interface | type | class | Zod schema | |
|---|---|---|---|---|
| Runtime existence | No | No | Yes | Yes |
| Runtime validation | No | No | No | Yes |
| Type inference | Is the type | Is the type | Is the type | z.infer<typeof S> |
Instantiable (new) | No | No | Yes | No |
| Composability | extends | &, | | extends | .merge(), .extend(), .and(), .or() |
| Parsing / coercion | No | No | No | Yes |
interface
Describes an object shape. Supports declaration merging — two declarations with the same name are merged:
interface User {
id: number;
name: string;
}
interface User {
email: string; // merged into the same interface
}
const user: User = { id: 1, name: "Alice", email: "[email protected]" };Extend with extends:
interface Admin extends User {
role: string;
}type
An alias for any type expression — objects, unions, intersections, tuples, mapped types:
type ID = string | number;
type Point = { x: number; y: number };
type Named = { name: string };
type NamedPoint = Point & Named; // intersectionUnlike interface, type cannot be re-opened for declaration merging.
class
Exists at runtime. Use when you need instantiation, methods, or inheritance:
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:
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: "[email protected]" });
// 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):
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:
// 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());