TypeScript/Types : Différence entre versions

De WikiSys
Aller à : navigation, rechercher
 
(Aucune différence)

Version actuelle en date du 11 septembre 2019 à 14:25

Advanced Types

Discriminated Unions

You can combine singleton types, union types, type guards, and type aliases to build an advanced pattern called discriminated unions, also known as tagged unions or algebraic data types.

Discriminated unions are useful in functional programming. Some languages automatically discriminate unions for you; TypeScript instead builds on JavaScript patterns as they exist today.

There are three ingredients:

  • Types that have a common, singleton type property — the discriminant.
  • A type alias that takes the union of those types — the union.
  • Type guards on the common property.
interface Square {
   kind: "square";
   size: number;
}
interface Rectangle {
   kind: "rectangle";
   width: number;
   height: number;
}
interface Circle {
   kind: "circle";
   radius: number;
}

First we declare the interfaces we will union. Each interface has a kind property with a different string literal type. The kind property is called the discriminant or tag. The other properties are specific to each interface. Notice that the interfaces are currently unrelated. Let’s put them into a union:

type Shape = Square | Rectangle | Circle;

Now let’s use the discriminated union:

function area(s: Shape) {
   switch (s.kind) {
       case "square": return s.size * s.size;
       case "rectangle": return s.height * s.width;
       case "circle": return Math.PI * s.radius ** 2;
   }
}

Exhaustiveness checking #

We would like the compiler to tell us when we don’t cover all variants of the discriminated union. For example, if we add Triangle to Shape, we need to update area as well:

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
   switch (s.kind) {
       case "square": return s.size * s.size;
       case "rectangle": return s.height * s.width;
       case "circle": return Math.PI * s.radius ** 2;
   }
   // should error here - we didn't handle case "triangle"
}

There are two ways to do this. The first is to turn on --strictNullChecks and specify a return type:

function area(s: Shape): number { // error: returns number | undefined
   switch (s.kind) {
       case "square": return s.size * s.size;
       case "rectangle": return s.height * s.width;
       case "circle": return Math.PI * s.radius ** 2;
   }
}

Because the switch is no longer exhaustive, TypeScript is aware that the function could sometimes return undefined. If you have an explicit return type number, then you will get an error that the return type is actually number | undefined. However, this method is quite subtle and, besides, --strictNullChecks does not always work with old code.

The second method uses the never type that the compiler uses to check for exhaustiveness:

function assertNever(x: never): never {
   throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
   switch (s.kind) {
       case "square": return s.size * s.size;
       case "rectangle": return s.height * s.width;
       case "circle": return Math.PI * s.radius ** 2;
       default: return assertNever(s); // error here if there are missing cases
   }
}

Here, assertNever checks that s is of type never — the type that’s left after all other cases have been removed. If you forget a case, then s will have a real type and you will get a type error. This method requires you to define an extra function, but it’s much more obvious when you forget it.

Polymorphic this types #