TypeScript is JavaScript with types added. Types are rules about what values a variable, argument, or function return value can hold. The compiler checks these rules before the code runs and gives hints right in the editor. Let's go through the basic types in order, starting from scratch.
Why TypeScript on top of JavaScript
In JavaScript, errors in working with data only surface at runtime. A typo in a field name, passing a string where a number was expected, forgetting that a function sometimes returns undefined — and you find out when the service has already crashed in production.
TypeScript adds a layer of checks before running. The tsc compiler reads type annotations and complains about incompatibilities upfront.
function double(n: number): number {
return n * 2;
}
double("10"); // error before running: string instead of number
An important detail: types only exist at compile time. After building they are erased, and at runtime plain JavaScript runs — there are no types in the compiled output. TypeScript protects the developer, but does not validate data coming from outside (from an HTTP request, from the database). External data still needs to be validated manually.
Short rule: TypeScript = JavaScript + type checking at build time.
Type annotations
An annotation is a : type written after a name. This is how the developer explicitly tells the compiler what is expected here.
const port: number = 8080;
const host: string = "localhost";
const debug: boolean = false;
function greet(name: string): string {
return `Hello, ${name}`;
}
Basic data types: number (integers and decimals — there is no separate type for integers), string, boolean, null, undefined. Arrays are written as number[] or string[]. Objects are described by listing their fields.
const ids: number[] = [1, 2, 3];
const user: { id: number; name: string } = {
id: 1,
name: "Anna",
};
Type inference
Annotations aren't needed everywhere. TypeScript can infer the type from the value on its own — this is called type inference.
const port = 8080; // inferred as number
const host = "localhost"; // inferred as string
const ids = [1, 2, 3]; // inferred as number[]
Here port still has type number even without an annotation: the compiler looked at the value on the right. That's why annotations are usually placed where inference is impossible or unreliable — on function arguments and sometimes on its return value. Inside the function body and for local variables, inference is trusted.
Short rule: annotate boundaries (arguments, return value), trust inference inside.
type and interface
When the shape of an object recurs, it gets a name. There are two ways: type and interface.
interface User {
id: number;
name: string;
}
type Product = {
id: number;
price: number;
};
Both describe the shape of an object, and in this case they are nearly interchangeable. The differences are in the details:
interfacecan be extended viaextendsand augmented by a repeated declaration with the same name (declarations merge). This is convenient for public contracts and library type descriptions.typecan do more: it gives a name not only to an object but to any type — a union, intersection, tuple, or primitive.
type Id = number; // alias for a primitive
type Point = [number, number]; // tuple
type Status = "active" | "banned"; // union (see below)
A practical starting rule: use interface for the shape of an object, type for everything else (unions, aliases, combinations). Teams often pick one for consistency — that's fine too.
Unions and intersections
A union with | means "either this or that".
type Id = number | string;
function findUser(id: number | string) {
// id can be either a number or a string
}
A common case — a value that may be absent:
let token: string | null = null;
token = "abc123";
An intersection with & means "both at the same time" — it merges the fields of multiple types into one.
interface HasId {
id: number;
}
interface HasTimestamps {
createdAt: Date;
}
type Entity = HasId & HasTimestamps;
// Entity has both id and createdAt
const row: Entity = {
id: 1,
createdAt: new Date(),
};
Literal types and optional fields
A literal type is a type consisting of a single specific value. On its own it is rarely useful, but inside a union it becomes a handy set of allowed options (similar to an enum).
type Role = "admin" | "user" | "guest";
function setRole(role: Role) {
// cannot pass an arbitrary string here,
// only one of the three values
}
setRole("admin"); // ok
setRole("owner"); // error: no such option
An optional field is marked with ? — it can be omitted. The type of such a field automatically becomes "value or undefined".
interface CreateUser {
name: string;
email?: string; // optional field
}
const a: CreateUser = { name: "Anna" }; // ok
const b: CreateUser = { name: "Bob", email: "b@x.io" }; // ok too
any, unknown and never
Three special types that are important to distinguish.
any — "turn off checking for this value". You can do anything with it, and the compiler stays silent. This is dangerous: any infects neighbouring code and nullifies all the benefits of TypeScript — errors surface at runtime again.
let data: any = fetchSomething();
data.foo.bar.baz; // no error at build time — crashes at runtime
const n: number = data; // also passes
unknown — the safe alternative. This too means "unknown", but the compiler does not allow any operations on such a value until you prove its type with a check.
let data: unknown = fetchSomething();
// data.foo; // error: type is still unknown
if (typeof data === "string") {
data.toUpperCase(); // safe here: inside the check it is a string
}
So for external data (request body, third-party API response) use unknown, not any, and check the type before use. How exactly to narrow types with checks is a separate topic — see links below.
never — a type that has no values at all. It appears where a value cannot exist: a function that always throws, or a branch of code that can never be reached.
function fail(message: string): never {
throw new Error(message);
}
Short rule: any — disables types (avoid it), unknown — safe "don't know yet", never — no value is possible.
In short
- TypeScript adds type checking at build time on top of JavaScript; at runtime types are erased and external data still needs to be validated.
- The annotation
: typesets the type explicitly; type inference often does it for you — annotate function boundaries, trust inference inside. interface— for the shape of an object (extendable, mergeable);type— for unions, intersections, aliases and everything else.union(|) — "one or the other";intersection(&) — "all at once".- Literal types in a union define a set of allowed values;
?makes a field optional. anydisables checks and is dangerous;unknownis the safe replacement requiring an explicit check;neveris the type with no values.
What to read next
- JavaScript essentials — the language foundation that types are built on.
- Type narrowing and type guards — how to safely work with unions and
unknown. - Generics — how to describe types that work with any data.