Appearance
What TypeScript Adds—and What It Cannot Add
TypeScript is JavaScript plus a static type language and a checker. Its central bargain is simple: describe and infer relationships before execution, then erase those descriptions so JavaScript can run.
ts
function total(values: number[]): number {
return values.reduce((sum, value) => sum + value, 0);
}The checker rejects total([1, "2"]). The emitted JavaScript contains no number[] or return annotation:
js
function total(values) {
return values.reduce((sum, value) => sum + value, 0);
}JavaScript semantics still determine coercion, equality, objects, promises, and errors. TypeScript neither replaces the runtime nor automatically validates values arriving from it.
Inference does most local work
TypeScript infers types from initializers and control flow:
ts
const retries = 3; // type: 3
let currentAttempt = 0; // type: number
const labels = ["new", "paid"]; // type: string[]Annotate boundaries where a contract helps readers and callers; allow inference inside implementations:
ts
type Transaction = {
id: string;
amount: number;
note?: string;
};
function summarize(transactions: readonly Transaction[]) {
const total = transactions.reduce((sum, item) => sum + item.amount, 0);
return { count: transactions.length, total };
}The return type is inferred as { count: number; total: number }. Writing every local type repeats information and can obscure the important contracts.
An optional property may be absent:
ts
type User = {
id: string;
displayName?: string;
};This differs from a required property whose value can be undefined:
ts
type User = {
id: string;
displayName: string | undefined;
};readonly prevents assignment through that particular type:
ts
type Point = { readonly x: number; readonly y: number };It is compile-time and shallow, not runtime freezing.
Tuples describe fixed positions:
ts
type Coordinate = readonly [latitude: number, longitude: number];Use an object when names and future evolution matter more than compact positional data.
Function types describe calls
ts
type Formatter = (amount: number, currency: string) => string;
function renderAmounts(amounts: number[], format: Formatter): string[] {
return amounts.map(amount => format(amount, "USD"));
}Functions returning no meaningful value use void. A function that cannot complete normally can return never:
ts
function fail(message: string): never {
throw new Error(message);
}never is also useful when exhaustive control flow has eliminated every possible value.
unknown protects a boundary; any disables checking
any allows nearly every operation and flows unsafely through the program:
ts
declare const payload: any;
payload.customer.name.toUpperCase(); // checker permits everythingunknown accepts any value but requires proof before use:
ts
function readName(payload: unknown): string {
if (
typeof payload === "object" &&
payload !== null &&
"name" in payload &&
typeof payload.name === "string"
) {
return payload.name;
}
throw new Error("Payload has no valid name");
}Use unknown for JSON, caught values, plugin results, and untyped external input. Use any narrowly when migrating untyped code or interfacing with an API that truly cannot be modeled, and keep it from spreading.
Assertions are claims, not conversions
This compiles:
ts
type Config = { port: number };
const config = JSON.parse(text) as Config;No check is emitted. If the JSON contains { "port": "fast" }, config.port is still a string at runtime. An assertion tells the checker to trust you; it does not make the claim true.
Validate first and return a typed value from the validator:
ts
function parseConfig(value: unknown): Config {
if (
typeof value !== "object" ||
value === null ||
!("port" in value) ||
typeof value.port !== "number"
) {
throw new Error("Invalid configuration");
}
return { port: value.port };
}The non-null assertion value! is another unchecked claim. It is occasionally necessary when a framework guarantees an invariant the checker cannot see, but an explicit branch usually documents the failure better.
The satisfies operator checks compatibility while retaining a precise inferred type:
ts
type RouteTable = Record<string, { method: "GET" | "POST" }>;
const routes = {
health: { method: "GET" },
create: { method: "POST" },
} satisfies RouteTable;Unlike as RouteTable, this does not ask TypeScript to forget the specific keys.
Types have their own namespace
ts
export type { Transaction } from "./transaction.js";
import type { Transaction } from "./transaction.js";Type-only imports and exports are erased and make runtime dependencies clear. A class exists as both a runtime constructor and an instance type; an interface or type alias exists only during checking.
Type aliases can name unions, primitives, tuples, and object shapes. Interfaces focus on object-like contracts and can be augmented. For application code, either is usually fine; favor consistency and choose based on the construct you need rather than folklore.
A TypeScript project is still a JavaScript project
tsconfig.json defines the checker and emitter's view of the project:
json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noEmit": true
},
"include": ["src/**/*.ts"]
}strict enables a family of checks that provide a sounder baseline. target describes the JavaScript syntax that may be emitted. module and moduleResolution should match the runtime or bundler that will actually load files. noEmit is common when another tool transforms source.
Some runtimes can execute .ts by stripping erasable syntax, while bundlers often transform it without checking types. These workflows are convenient, but they do not make type checking automatic. Keep tsc --noEmit as an explicit script or editor process.
TypeScript prevents many inconsistent uses inside the code it can see. It cannot prove arbitrary business invariants, validate external data, prevent races, fix misunderstood JavaScript semantics, or guarantee that deployed files match checked source. Its greatest value comes from accurately modeling real runtime behavior.