Grokking TypeScript
Vote

0% completed

​

Union, Literal, Enum, and Intersection Types

Union Types

Literal Types

Enums

Type Aliases

Intersection Types

The Complete Order

Key Takeaways

What type fits a value that can be a string or a number, or a state that can take only three exact values? The previous lesson gave a type to each value of an online order, such as the quantity, the prices, and a tracking code of type string | null. That last type was already built from two other types, and the rest of the order needs more types of that kind. We will add them one feature at a time, and the complete Order type appears at the end.

Union Types

An order reference may be a string in one system and a number in another. A union type allows a variable to be one of several types, and you write it with |:

let orderReference: string | number = "A-100"; // A string or a number orderReference = 100; // Allowed

Use a union when a value really can have different types, such as user input. The cost is that you may use only what all members of the union share. To use anything more, you must first check which member you have, and the compiler then narrows the type inside that branch:

function formatReference(reference: string | number) { reference.toUpperCase(); // Error if (typeof reference === "string") { return reference.toUpperCase(); // Allowed: reference is a string here } return reference.toFixed(2); // Allowed: reference is a number here }
reference.ts(2,13): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.

Literal Types

The status of an order is a string, but not just any string. A literal type allows one exact value instead of a general type, and a union of literal types allows a short list of exact values:

let orderState: "pending" | "shipped" | "delivered" = "pending"; orderState = "shipped"; // Allowed orderState = "lost"; // Error
state.ts(3,1): error TS2322: Type '"lost"' is not assignable to type '"delivered" | "pending" | "shipped"'.

Literal types suit the states of a user interface, such as "loading", "success", and "error". They also suit configuration flags, such as "yes" and "no", or a list of specific modes.

You have already been using them without seeing them. A const can never change, so its inferred type is the literal value. A let can change, so the compiler widens its inferred type to the general type:

const region = "EU"; // Inferred as the literal type "EU" let carrier = "DHL"; // Inferred as string

Enums

An enum defines a set of named constants, which helps with fixed sets of related values such as days of the week, colors, or an account status with the values ACTIVE, PENDING, and SUSPENDED. By default, its values are numbers that start at 0:

enum OrderStep { Placed, Packed, Shipped } console.log(OrderStep.Placed, OrderStep.Packed, OrderStep.Shipped);
0 1 2

If you set a starting value, the members after it continue from that value. Down, Left, and Right here receive 2, 3, and 4, and you could also write every value explicitly:

enum Direction { Up = 1, Down, Left, Right }

A string enum assigns a string to each member instead:

enum ShippingSpeed { Standard = "STANDARD", Express = "EXPRESS" } let speed: ShippingSpeed = ShippingSpeed.Express; // Allowed speed = "EXPRESS"; // Error
speed.ts(7,1): error TS2322: Type '"EXPRESS"' is not assignable to type 'ShippingSpeed'.

Enums differ from the other features in this lesson because they exist at runtime. The compiler turns an enum into a real JavaScript object. For a numeric enum, that object maps in both directions, so OrderStep[1] gives "Packed". A string enum maps only from member name to value.

The compiler turns the OrderStep enum into a JavaScript object that maps names to numbers and numbers back to names.
The compiler turns the OrderStep enum into a JavaScript object that maps names to numbers and numbers back to names.

Numeric enums also have a loose rule. The compiler rejects let step: OrderStep = 99 with error TS2322, because 99 is a known value outside the enum, but it allows any variable of type number to be assigned to an OrderStep.

An enum costs more than a literal union because it adds code to your output, and tools that only remove types cannot process it. Node.js, for example, rejects an enum in a .ts file with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. Choose an enum when you want a runtime object with named members, and choose a literal union when you only need the compile-time check.

Type Aliases

The union of order states would be tiring to repeat everywhere. A type alias gives a new name to an existing type or combination of types:

type ID = string | number; let userId: ID = "1234"; // Allowed

An alias creates only a name, not a separate type, so a value of type ID and a value of type string | number can be assigned to each other. Aliases can name any type, including tuples, objects, and function types:

type Point = [number, number]; let coordinate: Point = [10, 20]; type Buyer = { name: string; age: number }; let firstBuyer: Buyer = { name: "Bob", age: 30 }; // A function type: takes a string and returns a string. A later chapter covers these. type Formatter = (text: string) => string;

Use aliases to simplify complex type definitions, especially when you reuse them across a codebase.

Intersection Types

A union means that a value is one type or another. An intersection type, written with &, means that a value must satisfy all the types at once, so an object of that type needs the properties of each:

type Person = { name: string; age: number; }; type Contact = { email: string; phone: string; }; type Customer = Person & Contact; const customer: Customer = { name: "John", age: 30, email: "john@example.com", phone: "123-456-7890" };

If you remove phone from that object, the compiler names the missing property and the type that requires it:

customer.ts(13,7): error TS2322: Type '{ name: string; age: number; email: string; }' is not assignable to type 'Customer'. Property 'phone' is missing in type '{ name: string; age: number; email: string; }' but required in type 'Contact'.

Intersections work the same way with interfaces, which the chapter on object types covers. Use them to combine several types into a larger one, such as personal details and contact details. The picture below compares the two operators, using the types from this lesson.

A union value has one of the member types, while an intersection value has all the properties of every member.
A union value has one of the member types, while an intersection value has all the properties of every member.

The Complete Order

Most of these features now have a place in the order. The prices array and the string | null tracking code come from the previous lesson, and Point names the tuple type that lesson introduced. The other fields use the types from this lesson, and state uses a literal union and not an enum, because it needs only the compile-time check:

type ID = string | number; type OrderState = "pending" | "shipped" | "delivered"; type Point = [number, number]; type Person = { name: string; age: number }; type Contact = { email: string; phone: string }; type Customer = Person & Contact; type Order = { id: ID; customer: Customer; prices: number[]; deliveryPoint: Point; state: OrderState; trackingCode: string | null; }; const order: Order = { id: "A-100", customer: { name: "John", age: 30, email: "john@example.com", phone: "123-456-7890" }, prices: [20, 5], deliveryPoint: [10, 20], state: "pending", trackingCode: null }; console.log(order.state, order.prices.length);
pending 2

Key Takeaways

  • A union is one type or another, and an intersection is all of them at once.
  • A literal union disappears at compile time, while an enum becomes a runtime object.
  • A type alias is a new name, not a new type.

Reading Progress

0%


Vote for new content

On This Page

Union Types

Literal Types

Enums

Type Aliases

Intersection Types

The Complete Order

Key Takeaways