TypeScript Keywords Reference
JavaScript has its own set of reserved words (let, function, class, and so on), but TypeScript layers a second vocabulary on top of that: words which only exist to describe types, not runtime behavior. Some of these, like interface and type, only exist at compile time and vanish completely once your code is compiled to JavaScript. Others, like enum and namespace, actually generate real runtime code. Knowing which is which — and what each keyword actually does — is essential to reading and writing TypeScript fluently. This lesson is a single reference page for every TypeScript-specific keyword, grouped by purpose.
Overview: two categories of keywords
Every TypeScript keyword falls into one of two buckets, and the distinction matters a lot in practice.
Type-space keywords exist purely for the type checker. The compiler reads them, uses them to verify your code, and then erases them entirely — the emitted JavaScript never mentions them. interface, type, keyof, typeof (in a type position), infer, is, as, satisfies, readonly (on type members), and declare all fall here. If you compiled a file that used nothing but these, and stripped out any runtime statements, you would get an empty (or near-empty) JavaScript file.
Value-space keywords (in TypeScript’s extended sense) produce actual JavaScript at runtime, but TypeScript adds extra rules or extra syntax around them. enum compiles to a real object. namespace compiles to an IIFE-wrapped object. abstract, implements, public/private/protected, static, override, and constructor-parameter readonly all modify classes, which are themselves real JavaScript constructs.
Understanding which bucket a keyword is in tells you immediately whether it affects your bundle size and runtime behavior, or whether it’s a compile-time-only annotation that disappears without a trace.
Syntax and quick reference table
The table below is the core reference. Skim it once, then use the worked examples to see the less obvious ones (infer, satisfies, in as a mapped-type modifier) in context.
| Keyword | Space | Purpose |
|---|---|---|
type |
Type | Declares a named type alias (object shape, union, tuple, mapped type, etc.) |
interface |
Type | Declares an object shape; supports declaration merging and extends |
enum |
Value | Declares a named set of numeric or string constants; compiles to a real object |
keyof |
Type | Produces a union of a type’s property names as string/number literals |
typeof |
Type (dual) | In a type position, extracts the static type of a value (variable, const, function) |
in |
Type | Iterates a union of keys inside a mapped type ([K in Keys]) |
infer |
Type | Introduces a type variable to be inferred inside a conditional type |
is |
Type | Marks a function as a custom type guard (value is string) |
as |
Type | Type assertion (override the checker) or rename in import ... as |
satisfies |
Type | Validates a value against a type without widening or losing its literal type |
asserts |
Type | Marks a function as an assertion function that narrows its parameter or throws |
readonly |
Type/Value | Marks a property or array/tuple as immutable to the type checker |
declare |
Type | Declares an ambient value that exists elsewhere, with no emitted code |
namespace |
Value | Groups related code under a single named object, compiled to real JS |
abstract |
Value | Marks a class or member that must be implemented by a subclass |
implements |
Value | Declares that a class satisfies an interface’s shape |
public / private / protected |
Value | Class member visibility, enforced only at compile time |
static |
Value | Declares a class member that belongs to the class itself, not instances |
override |
Value | Asserts a method intentionally overrides a base class method |
never / unknown / any |
Type | Special built-in types for "impossible", "unchecked-but-safe", and "opt out of checking" |
Examples
Example 1: interface, enum, readonly, and keyof together
This example shows the everyday keywords: an interface for shape, a readonly field that can’t be reassigned, an enum for a fixed set of states, and keyof to derive the list of valid property names from the interface itself (so the two never drift apart).
interface User {
readonly id: number;
name: string;
role?: "admin" | "member";
}
enum Status {
Active,
Inactive,
}
type UserKeys = keyof User; // "id" | "name" | "role"
function describe(user: User): string {
return `${user.name} is ${Status[Status.Active]}`;
}
const alice: User = { id: 1, name: "Alice", role: "admin" };
console.log(describe(alice));
const keys: UserKeys[] = ["id", "name", "role"];
console.log(keys);
Output:
Alice is Active
[ 'id', 'name', 'role' ]
Note that Status[Status.Active] works because a numeric enum generates a reverse mapping at runtime — Status.Active is 0, and Status[0] gives back the string "Active". Also note id is readonly: if you tried alice.id = 2 anywhere, tsc would report "Cannot assign to ‘id’ because it is a read-only property" — but nothing stops you from mutating it in plain JavaScript, since readonly is erased at compile time.
Example 2: satisfies, typeof, in, is, and infer
This example is denser and shows the more advanced type-space keywords working together: satisfies checks a literal against a shape while keeping its precise type, typeof pulls a type out of a value, a user-defined type guard uses is, and a conditional type uses infer to pull the element type out of an array type.
const config = {
host: "localhost",
port: 8080,
} satisfies Record;
type Config = typeof config;
function isString(value: unknown): value is string {
return typeof value === "string";
}
type ElementType = T extends (infer U)[] ? U : never;
type Item = ElementType; // resolves to number
function logHost(cfg: Config) {
if ("host" in cfg) {
console.log(`Host: ${cfg.host}`);
}
}
logHost(config);
const value: unknown = "hello";
if (isString(value)) {
console.log(value.toUpperCase());
}
const port = config.port as number;
console.log(`Port: ${port}`);
Output:
Host: localhost
HELLO
Port: 8080
The key detail is satisfies: it checks that config is assignable to Record<string, string | number> without actually changing config‘s inferred type to that wider type. Because of this, typeof config still knows port is specifically number (not the union), which is why config.port as number is a safe, redundant-but-legal assertion here. Had we written const config: Record<string, string | number> = {...} instead, we’d have widened the type and lost that precision.
Example 3: declare, namespace, abstract, and override
This example shows the class-related keywords, plus declare for an ambient value that’s assumed to exist without TypeScript emitting anything for it.
declare const VERSION: string;
namespace Shapes {
export abstract class Shape {
abstract area(): number;
describe(): string {
return `Area: ${this.area().toFixed(2)}`;
}
}
export class Circle extends Shape {
private radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
override area(): number {
return Math.PI * this.radius ** 2;
}
}
}
const circle = new Shapes.Circle(2);
console.log(circle.describe());
Output:
Area: 12.57
declare const VERSION tells the checker "trust me, this exists somewhere (perhaps injected by a build tool)" and emits nothing — it’s purely a promise to the compiler. abstract class Shape can’t be instantiated directly (new Shape() would be a compile error), and abstract area() forces every subclass to provide an implementation. override doesn’t change runtime behavior at all; it just tells the checker to verify that a same-named method really exists on the base class, catching typos when a base class method gets renamed.
Under the hood: what survives compilation
If you compile all three examples above and look at the output JavaScript, you’ll see the pattern clearly. The interface User, type UserKeys, type Config, type ElementType, and every : SomeType annotation disappear completely — there is zero trace of them in the emitted JS. keyof, typeof (in type position), infer, is, satisfies, and declare are compile-time-only instructions to the type checker; none of them produce a single byte of output.
By contrast, enum Status compiles into an actual object with both forward (Active: 0) and reverse (0: "Active") mappings built at runtime. namespace Shapes compiles into an immediately-invoked function that builds an object with Shape and Circle attached to it — Shapes.Circle is a real property access at runtime, not a type-only path. The class modifiers abstract, private, protected, and override are checked only while compiling; the emitted class has ordinary methods and properties, because JavaScript itself (pre-#private-fields) has no concept of member visibility enforced by the runtime.
Common Mistakes
Mistake 1: using as to force an incompatible type. A type assertion doesn’t convert a value — it just tells the compiler to trust you, which can produce runtime bugs the checker would otherwise have caught.
const value: unknown = "42";
const n = value as number; // compiles, but 'n' is really still a string
console.log(n + 1); // "421", not 43
This compiles without error (which is exactly the danger), because as only asserts — it performs no conversion and no runtime check. Use Number(value) or a validated type guard instead of asserting across unrelated types.
Mistake 2: forgetting that declare creates no runtime value.
declare const API_KEY: string;
console.log(API_KEY.length);
This type-checks fine — tsc trusts the declaration — but if nothing in your actual build ever defines a global API_KEY, running the compiled JavaScript throws ReferenceError: API_KEY is not defined. declare is a promise to the compiler, not a guarantee about the runtime environment; you must ensure the value really is injected (via a script tag, bundler define, or global assignment) before use.
Best Practices
- Prefer
interfacefor object shapes that might be extended or merged, andtypefor unions, tuples, and mapped types —interfacecan’t express a union directly. - Reach for
satisfiesinstead of an explicit type annotation whenever you want validation without losing literal/narrow types. - Avoid casting with
asacross unrelated types; if you need runtime confidence, write anis-based type guard or anassertsfunction instead. - Use
keyof typeofto derive literal unions from existing runtime objects (like config maps) instead of hand-maintaining a parallel type. - Avoid
namespacein modern module-based code — it predates ES modules; prefer regularimport/exportunless you’re authoring global ambient type declarations. - Add
overrideto every subclass method that’s meant to replace a base class method, so renames in the base class surface as compiler errors instead of silent bugs.
Practice Exercises
- Exercise 1: Write an
interface Productwithid,name, andprice, then usekeyofto create atype ProductKeyand a functiongetField(product: Product, key: ProductKey)that returns the corresponding value. - Exercise 2: Write a custom type guard function
isNumberArray(value: unknown): value is number[]and use it to safely sum an array only when the guard passes. - Exercise 3: Declare a
const settings = { theme: "dark", retries: 3 } satisfies Record<string, string | number>, then write a function that logssettings.themein uppercase — confirm thattypeof settingsstill reportsthemeas the literal typestringrather than a wider union.
Summary
- TypeScript keywords split into type-space (erased at compile time:
type,interface,keyof,infer,is,satisfies,declare) and value-space (produce real JS:enum,namespace, class modifiers). keyofandtypeoflet you derive types from existing types or values, keeping them in sync automatically.satisfiesvalidates a value’s shape without widening its inferred type — prefer it over an explicit annotation when you want both.asis an assertion, not a conversion — it can silently hide real bugs if misused.declarepromises the compiler a value exists elsewhere; it emits nothing, so the runtime environment must actually provide it.abstract,override, and visibility modifiers (public/private/protected) are compile-time-only guarantees on top of ordinary JavaScript classes.
