Low Maintenance Types in TypeScript
Dependent types are an interesting feature in TypeScript: Not only do they show what is happening in a program. They also prepare the code for things to come.
- Stefan Baumgartner
(Dieser Artikel erschien ursprünglich in der englischsprachigen "We Are Developers!"-Beilage, die c't-Abonnenten mit der Ausgabe 13/2022 und iX-Abonnenten mit der Ausgabe 7/2022 erhalten. Weitere Informationen sind dem Ankündigungsbeitrag zu entnehmen.)
The ultimate goal of TypeScript is to formalize even the most versatile interfaces in JavaScript, an inherently flexible and dynamic programming language. TypeScript is a superset of JavaScript with a syntax that enables static typing. It adds a variety of operators and constructs to the language to describe those complex scenarios. But not only do they allow for basic formalism, they also let developers create dependencies between types. Those typing constructs can be used to create a set of low maintenance types: static types, which are dynamic enough to update themselves if certain conditions change.
TypeScript’s Type System
TypeScript’s type system has one simple rule: A type defines a set of compatible values. As an example, the number 1337.42 is a compatible value of the type number
. The string "Hello, World!” is compatible with the type string
. Both string
and number
are primitive types and define a theoretically endless set of values. The primitive type boolean
, on the other hand, only defines two values: true
and false
.
Interfaces and type aliases allow the description of complex JavaScript objects. These are called compound types, as they are usually an assembly of various properties of primitive types. The definition of a compound type also enables defining a set of compatible object values. For example, the type Person
type Person = {
age: number,
name: string
}
describes all objects which contain a property age
of the type number
, and a property name
of the type string
. Objects with additional properties are also compatible with the type Person
. The shape of object types is important. Object types are compatible with each other if the same properties have the same types:
type Person = {
age: number,
name: string
}
type Student = {
age: number,
name: string,
semester: number
}
const student: Student = {
age: 24,
semester: 7,
name: "Jane Doe"
}
const person: Person = student // this works!
Compared to other programming languages which—with a few notable exceptions—feature nominal type systems, TypeScript features a structural one: As long as the shape of an object is equal or similar, TypeScript considers this a valid type check. A nominal type system, on the other hand, would not only ensure the shape to be the same, but also the assigned name to be the same or to be in a compatible hierarchy.
Der vorliegende Artikel stammt aus der "We Are Developers!"-Beilage 2/2022, die der c't 13/2022 und der iX 7/2022 in gedruckter Form beiliegt. Im iX-Downloadbereich ist sie als PDF kostenfrei verfügbar.
Die Beilage richtet sich an professionelle Softwareentwicklerinnen und -entwickler. In dieser Ausgabe behandelt sie die Zukunft von Webframeworks, mögliche Angriffe auf Machine-Learning-Systeme und Expertenmeinungen zum aktuellen und künftigen Stand der Programmiersprache Java. Zudem hält die Sommerausgabe dieses anschauliche TypeScript-Tutorial bereit.
Das Magazin entsteht in Kooperation von heise Developer mit der IT-Job-Plattform WeAreDevelopers aus Wien, an der Heise beteiligt ist. Erstmals erschien das Magazin im Vorfeld des englischsprachigen WeAreDevelopers World Congress vom 14. bis 15. Juni 2022 komplett in englischer Sprache.
The reason for using a structural type system instead of a nominal one lies in the way JavaScript works: Object literals are everywhere, so a nominal type system would only prevent developers from using what already exists.
TypeScript not only enables developers to define a set of compatible values, but also to broaden this set or to narrow the set of compatible values as they go. The intersection type is a good example. In the following code, two types are defined: Person
and Studies
. The type Student
is an intersection of both types. It includes all values that are compatible with Person
as well as with Studies
, which narrows the set of possible values.
type Studies = {
semester: number
}
type Person = {
name: string,
age: number
}
type Student = Person & Student
This can be visualized easily through a traditional set diagram (Figure 1).
A union type works in the opposite direction. The following listing defines groups of people that may be encountered at a university. This example does not claim to be complete.
type Professor = Person & {
institute: string,
employeeId: string
}
type UniversityPeople = Student | Professor
The union set of Student
and Professor
is called UniversityPeople
, and is a larger set than both subsets. There is an intersection, namely professors who study, but there are also compatible values that just fall into one of the two categories (Figure 2).
TypeScript’s type system allows developers to define which values belong to which set. While union and intersection types are the most basic constructs it provides to define sets, they are just the beginning. TypeScript has much, much more up its sleeve.