Type Aliases and the type Keyword

7 min read

You've already used the type keyword a few times — type Severity = "critical" | "high" | "medium" | "low", type TestResult = [name: string, passed: boolean, duration: number]. This lesson formalises what that's doing: type aliases give a name to a type so you can reuse it across a project. Combined with the & operator, they let you build complex test data shapes by composing smaller ones. Aliases are how typed projects stay readable as they grow.

What a type alias is

A type alias is a name you give to a type. Once defined, you can use it anywhere a type is expected.

type StatusCode = number;
type TestName = string;
type UserId = string | number;
 
const code: StatusCode = 200;
const name: TestName = "Login test";
const id: UserId = "user-42";

There's no runtime difference — StatusCode is number once compiled. The benefit is for humans: function signatures and type errors talk in your domain's vocabulary instead of generic primitives. getStatus(): StatusCode reads better than getStatus(): number.

Object type aliases

Aliases shine when the type is bigger than a primitive. An object shape, given a name, can be used in dozens of places:

type TestResult = {
  name: string;
  status: "passed" | "failed" | "skipped";
  duration: number;
  error?: string;        // optional — only present if failed
};
 
const result: TestResult = {
  name: "Login",
  status: "passed",
  duration: 1250,
};
 
function format(r: TestResult): string {
  return `${r.name}: ${r.status} (${r.duration}ms)`;
}

The error?: syntax marks the field as optional — result.error is string | undefined and may be omitted. You'll meet optional fields again properly in chapter 3.

Intersection types — combining shapes

Intersection types use the & operator and read as "AND." The resulting type has every field from every component:

type Timestamps = {
  createdAt: string;
  updatedAt: string;
};
 
type User = {
  name: string;
  email: string;
};
 
type UserWithTimestamps = User & Timestamps;
// Has all four fields: name, email, createdAt, updatedAt
 
const u: UserWithTimestamps = {
  name: "Alice",
  email: "alice@example.com",
  createdAt: "2026-01-12T10:00:00Z",
  updatedAt: "2026-05-04T08:14:00Z",
};

Try omitting one of the four fields and the compiler complains — "Property 'updatedAt' is missing in type."

Union vs intersection — "or" vs "and"

Two operators that look similar and do the opposite thing:

  • Union (|) — the value is one type OR another.

    type Id = string | number;       // either a string or a number
  • Intersection (&) — the value has fields from one type AND another.

    type Both = User & Timestamps;   // every field from both

A useful mnemonic: | is broader (more values possible), & is narrower (more fields required). They sound symmetric but point in opposite directions.

When to alias and when to inline

A type alias earns its keep when:

  • You use the same shape in more than one place. Aliasing means renames happen once.
  • The type is complex enough to be hard to read inline — long unions, nested objects, or anything where the annotation would dwarf the function signature.
  • The alias name adds meaning beyond the structure. UserId says more than string, even if the underlying type is identical.

Inline types are fine for one-off shapes used in a single function. Don't alias just because aliasing is available.

// Fine inline — used once, simple
function logEntry(entry: { time: string; message: string }) { /* ... */ }
 
// Worth aliasing — reused, conveys meaning
type ApiError = { code: string; message: string; retryable: boolean };
function reportError(e: ApiError) { /* ... */ }
function isRetryable(e: ApiError): boolean { return e.retryable; }

Naming convention

Type aliases use PascalCaseTestResult, ApiResponse, UserCredentials. Same convention as classes and interfaces (which you'll meet next chapter). The convention helps readers spot at a glance whether a name refers to a type (TestResult) or a value (testResult).

Composing test data with intersections

A real test suite often has layered types — every entity has timestamps, every user has permissions, every request has a correlation id. Intersection lets you build these by composition rather than copy-paste.

FullUser = User & Timestamps & Permissions
  • – name: string
  • – email: string
  • – createdAt: string
  • – updatedAt: string
  • role: 'admin' | 'tester' –
  • scopes: string[] –

In code:

type Timestamps = { createdAt: string; updatedAt: string };
type User = { name: string; email: string };
type Permissions = { role: "admin" | "tester"; scopes: string[] };
 
type FullUser = User & Timestamps & Permissions;
 
const alice: FullUser = {
  name: "Alice",
  email: "alice@example.com",
  createdAt: "2026-01-12T10:00:00Z",
  updatedAt: "2026-05-04T08:14:00Z",
  role: "admin",
  scopes: ["users:read", "users:write"],
};

You can build TestCase, TestSuite, and TestRun the same way — small named pieces, intersected into the bigger shape your code actually deals with. The same intersection scales: a fixture loader returning User & Timestamps, a test factory adding Permissions only for admin scenarios, an analytics call adding RequestId.

A note on type vs interface

You'll learn interface properly in chapter 3. The short version: for object shapes, type and interface are interchangeable for most uses. interface supports declaration merging (multiple interface User {} blocks in different files merge into one); type supports unions, tuples, mapped types, and conditional types — things interface can't express. A pragmatic rule many teams use: prefer interface for object shapes, reach for type when the shape isn't a plain object (unions, intersections, tuples, mapped types).

⚠️ Common mistakes

  • Aliasing for a single-use type. type EnvName = "dev" | "staging"; function deploy(env: EnvName) {} — fine if EnvName is reused. If only deploy ever takes it, inline function deploy(env: "dev" | "staging") {}. Aliases are documentation; one-off aliases are noise.
  • Confusing & with |. type Result = Success & Failure is not "either success or failure" — it's "has all the fields of both," which is usually nonsense. For "either," use |. The two operators read similarly in English but mean opposite things in the type system.
  • Forgetting that intersection requires every property. type A & B is a value with all fields from both. If two intersected types declare the same property with different types (e.g., both have id but one is string and the other is number), TypeScript narrows the field to the impossible intersection (never) and your variable becomes unusable. Keep intersected types disjoint.

🎯 Practice task

Compose a type-safe test data model. 20-30 minutes.

  1. In your ts-for-qa/src folder, create model.ts.
  2. Declare these primitive aliases:
    • type TestId = string
    • type DurationMs = number
    • type TestStatus = "passed" | "failed" | "skipped"
  3. Declare three small object aliases:
    • type TestCase = { id: TestId; name: string; status: TestStatus; duration: DurationMs }
    • type Timestamps = { startedAt: string; finishedAt: string }
    • type Environment = { browser: "chromium" | "firefox" | "webkit"; baseUrl: string }
  4. Compose them: type TestRun = TestCase & Timestamps & Environment. The resulting type has every field from all three.
  5. Create one TestRun value, populate every field, and write a function summarise(run: TestRun): string that returns a one-line summary.
  6. Run with npx ts-node src/model.ts. Confirm the output.
  7. Try the union mistake. Change TestRun = TestCase | Timestamps | Environment (note the |). Hover over a field access — TypeScript narrows the type to only the intersection of fields, and most accesses become errors. Read why, then revert.
  8. Stretch: add type RetryInfo = { attempts: number; lastError?: string } and intersect it into TestRun only when retries are enabled. Notice how the optional ? on lastError lets you omit the field while still having it typed.

You've now seen every basic type-system tool — primitives, arrays, tuples, unions, literals, aliases, intersections. The next chapter introduces interface, the canonical way to describe object shapes for the rest of your career.

// tip to track lessons you complete and pick up where you left off across devices.