In this note, we will explore some advanced types and features in TypeScript that can help you write more expressive and robust code. We will cover type aliases, union and intersection types, enums, abstract classes, generics, tuples, and decorators.
type
KeywordIn TypeScript, you can define a type using the type
keyword, allowing you to associate a name with a type and use it wherever you need it. Types can be used to define complex structures easily.
In TypeScript, both type
and interface
can be used to define the shape of an object, but there are subtle differences and preferences when to use which.
Interfaces are preferred when defining the structure for object literals due to their extendable and more powerful capabilities
Types are more versatile and can represent a wider range of structures including primitive types, union types, and intersection types (more on this later)
TypeScript allows for composing types using union and intersection types, allowing developers to combine existing types to create new ones.
Union types are helpful when a value can be one of several types, represented by the |
symbol.
Intersection types are represented using the &
symbol and are used when a value should be of multiple types.
Note: You cannot use &
or |
directly in an interface declaration. However, you can use them in type aliases.
|
) - “OR”In TypeScript, the Union type (denoted with |
) is used when a value can be one of several types. You can think of it as an “OR” operation. When you define a type as TypeA | TypeB
, you’re saying that a value of that type can be either of TypeA
or TypeB
.
Example:
&
) - “AND”The Intersection type (denoted with &
) is used when you want a type to have all the properties of several types. You can think of it as an “AND” operation. If you have TypeA & TypeB
, then a value of this type will have all the properties of both TypeA
and TypeB
.
Example:
Now, from a set theory standpoint, the names “Union” and “Intersection” might seem reversed, but here’s how to understand them:
Union (|
): Refers to the union of two sets. In set theory, the union of two sets A and B is the set of elements which are in A, in B, or in both A and B. Hence in TypeScript, a value of type A | B
can be any member of A, or any member of B (or both if the types have overlap).
Intersection (&
): Refers to what is common between two sets. In set theory, the intersection of two sets A and B is the set that contains all elements of A that also belong to B (or equivalently, all elements of B that also belong to A). In TypeScript’s type system, when you do A & B
, you’re taking what’s common (and extending) between the type definitions, effectively combining them.
Type aliases in TypeScript allow you to create a new name for a type. It can represent a primitive type, union type, and any other types that you would otherwise have to write out by hand. Type aliases are created using the type
keyword and are particularly useful for ensuring consistent usage of complex types across your codebase, enhancing readability and maintainability.
In this example, StringOrNumber
is a type alias for a union type, Coordinate
is a type alias for an object type representing a point in a 2D space, and Callback
is a type alias for a function type that returns no value.
By using type aliases, developers can avoid rewriting complex type annotations, make the code more self-explanatory, and simplify any future changes to the type definition, as it needs to be updated in only one place.
TypeScript employs a structural type system, which focuses on the shape that values have. This system is more concerned with the properties and methods of a value, rather than its nominal type or the name of the type it was declared with.
In TypeScript’s structural type system, types can be understood as sets of values. A type is considered a set of possible values that it can hold, and a value is a member of a type if it has all the properties that the type expects, with appropriate types of their own. This is a significant shift from nominal typing systems where types are associated with names and the relationship between them is determined by explicit declarations.
TypeScript’s structural types are erased during the compilation process, and do not exist at runtime. This means that the type information is used solely for type checking during development and is not available for reflection or any runtime type operations. This design choice allows TypeScript to stay close to JavaScript’s runtime behavior while providing a robust type system for development.
The erasure of structural types means that TypeScript does not support reflection based on type information, as the type annotations do not exist at runtime. This can be a limitation when developers need to perform operations based on type information at runtime.
Since structural typing is based on the properties of types, an object with no properties can be assigned to any type, making empty objects assignable to anything.
Two different types with identical properties are considered the same type in a structural type system. This allows for more flexible and intuitive type assignments.
Enums, short for “enumerations”, are a feature in TypeScript that allows for defining named constants, making code more readable and expressive. They are particularly useful when representing a fixed set of related values, like days of the week, colors, or directions.
In TypeScript, you define an enum using the enum
keyword:
By default, the first value of an enum starts at 0
, and each subsequent value is incremented by one. In the example above, Days.Monday
would have a value of 0
, Days.Tuesday
would have a value of 1
, and so on.
You can also assign custom values to enums:
Enums can help improve code clarity. Instead of using hard-coded values, you can use descriptive names:
Enums can also have computed members:
In the above enum, Read
, Write
, and ReadWrite
are computed members, while None
is a constant.
Abstract classes are base classes from which other classes may be derived. They may not be instantiated directly and may contain implementation details for their members. The abstract
keyword is used to define abstract classes and abstract methods within them.
Generics provide a way to create reusable components which can work over a variety of types rather than a single one. Generics are particularly useful when dealing with data structures, allowing them to work with any type while maintaining type safety.
In TypeScript, a tuple is a special type that allows you to create an array where the type of a fixed number of elements is known, but need not be the same. It enables you to represent a value as a combination of different types.
In the example above, the student
tuple has a string as its first element and a number as its second element. Assigning values in a different order or introducing elements of a different type would result in an error.
Tuples are useful when you want to create a quick, fixed-size collection of elements of varied types without creating a class or interface. For example, if you want to represent a Point
in a 2D space, you can use a tuple of two numbers:
You can access the elements of a tuple using indexing, just like an array. However, accessing an element outside the known indices will result in an error.
TypeScript also allows you to have optional elements in a tuple, represented using the ?
symbol, and rest elements, represented using the ...
symbol, allowing more flexibility in handling tuples with varying lengths.
In this example, Person
is a tuple with an optional second element, allowing it to either have one or two elements, and Numbers
is a tuple with a rest element, allowing it to have any number of elements, with at least one.
Tuples, with their ability to represent multiple types in a single, ordered collection, provide a way to store related values without creating a structured type, making them a handy feature in TypeScript’s type system.
Decorators are an advanced topic beyond the scope of this course. However, you may work with frameworks and libraries that make use of decorators. Therefore, we will briefly discuss them.
Decorators provide a way to add annotations and a meta-programming syntax for class declarations and members. Decorators are denoted by the @
symbol and can be used to modify classes, properties, methods, and parameters.