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.

In Pocket speichern vorlesen Druckansicht
Business,Concept,-,High,Speed,Abstract,Mrt,Track,Of,Motion

(Bild: voyata/Shutterstock.com)

Lesezeit: 14 Min.
Von
  • Stefan Baumgartner
Inhaltsverzeichnis

(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 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.

"We Are Developers!"-Beilage 2/2022

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).

The intersection type Student defines the intersection of Person and Studies (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

Union type, the union of Student and Professor (Figure 2)

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.