Ich kenne was, was Du nicht kennst: check-types

Tales from the Web side  –  0 Kommentare

Insbesondere Sprachneulinge dürften anfangs über den ein oder anderen typbedingten Fehler bei der JavaScript-Entwicklung stoßen. Typüberprüfungen in JavaScript sind nämlich so eine Sache: Was war nochmal der Unterschied zwischen dem typeof- und dem instanceof-Operator? Und was der Unterschied zwischen null und undefined? Warum gibt der typeof-Operator für Arrays und reguläre Ausdrücke den Wert "object" zurück, für Funktionen aber den Wert "function" (obwohl diese ja eigentlich auch Objekte sind)?

"Ich kenne was, was Du nicht kennst"

... ist eine gemeinsame Serie von Golo Roden und Philip Ackermann, in der die beiden regelmäßig Module für JavaScript und Node.js vorstellen.

Überhaupt funktioniert der typeof-Operator recht unzuverlässig bzw. besser gesagt: nicht so, wie man es als Entwickler vielleicht erwarten würde. Die Beispiele in folgendem Listing demonstrieren das: bis auf die Unterscheidung der einzelnen primitiven Datentypen sowie Funktionen und dem besonderen Wert undefined liefert der Operator immer den Wert "object" (übrigens auch für null, was historisch bedingt ist und in der ECMAScript-Spezifikation nie angepasst wurde).

console.log(typeof {});             // object
console.log(typeof function () {}); // function
console.log(typeof []); // object
console.log(typeof 2); // number
console.log(typeof ''); // string
console.log(typeof true); // boolean
console.log(typeof new Date()); // object
console.log(typeof /tests/i); // object
console.log(typeof null); // object
console.log(typeof undefined); // undefined

Um genauer herauszufinden, um welchen Typ von Objekt es sich jeweils handelt, muss man also weitere Tests machen. Zum Beispiel den instanceof-Operator hinzuziehen oder sich – wie im folgenden Beispiel gezeigt – die Tatsache zunutze machen, dass ein Aufruf der Methode toString() auf die interne Eigenschaft [[Class]] zugreift und deren Wert ausliest:

Object.prototype.toString.call(value);

Das Ganze dann in eine Helferfunktion verpackt, liefert schon brauchbarere Ergebnisse:

function getType(value) {
return Object.prototype.toString.call(value);
}

console.log(getType({})); // [object Object]
console.log(getType(function () {})); // [object Function]
console.log(getType([])); // [object Array]
console.log(getType(2)); // [object Number]
console.log(getType('')); // [object String]
console.log(getType(true)); // [object Boolean]
console.log(getType(new Date())); // [object Date]
console.log(getType(/tests/i)); // [object RegExp]
console.log(getType(null)); // [object Null]
console.log(getType()); // [object Undefined]

Doch damit ist es immer noch nicht ganz getan: Es warten noch weitere Stolperfallen und Spezialfälle, die zu beachten sind. Kurz gesagt: Typüberprüfungen in JavaScript können recht aufwendig sein. Es stellt sich also die Frage, ob es nicht für all das ein Node.js-Modul gibt, das einem diese Arbeit abnimmt. Die Antwort lautet: ja, das gibt es.

Das Modul check-types

Es gibt sogar gleich mehrere Module, wobei aber das Modul check-types eines derjenigen ist, welches am aktivsten weiter entwickelt wird. Installieren lässt sich check-types per NPM als lokale Abhängigkeit über den Befehl npm install check-types oder global über npm install -g check-types. Anschließend lässt es sich wie gewohnt über require() einbinden:

let t = require('check-types'); 

(Alternativ kann check-types auch per <script>-Tag im Browser eingebunden werden. In diesem Fall stellt das Modul das globale Objekt check zur Verfügung).

Für die in den vorigen Listings aufgezählten Typen stellt check-types (mit Ausnahme regulärer Ausdrücke) jeweils eine gleichnamige Methode zur Verfügung, die jeweils prüft, ob es sich bei dem übergebenen Wert um den entsprechenden Typen handelt:

console.log(t.object({}));                // true
console.log(t.function(function () {})); // true
console.log(t.array([])); // true
console.log(t.number(2)); // true
console.log(t.string('')); // true
console.log(t.boolean(true)); // true
console.log(t.date(new Date())); // true
console.log(t.null(null)); // true
console.log(t.undefined()); // true

Überhaupt ist die gesamte API des Moduls sehr darauf ausgerichtet, lesbaren Code zu produzieren. Um beispielsweise einen Aufruf zu negieren, schaltet man einfach ein not dazwischen:

console.log(t.not.object({}));                // false
console.log(t.not.function(function () {})); // false
console.log(t.not.array([])); // false
console.log(t.not.number(2)); // false
console.log(t.not.string('')); // false
console.log(t.not.boolean(true)); // false
console.log(t.not.date(new Date())); // false
console.log(t.not.null(null)); // false
console.log(t.not.undefined()); // false

Möchte man erlauben, dass beim Aufruf von beispielsweise number() der übergebene Wert auch null oder undefined sein kann, erreicht man dies über ein dazwischen geschaltetes maybe:

console.log(t.number(null));              // false
console.log(t.maybe.number(null)); // true
console.log(t.number(undefined)); // false
console.log(t.maybe.number(undefined)); // true
console.log(t.maybe.number('')); // false

Ob ein Wert eine Objektinstanz einer Klasse (bzw. eines Prototypen) ist, lässt sich über die Methode instance() ermitteln:

'use strict';
let t = require('check-types');
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
let max = new Person('Max', 'Mustermann');
console.log(t.instance(max, Person)); // true

Auch das Überprüfen von "typisierten Arrays" ist möglich:

let numbers = [2,3,4,5];
console.log(t.array.of.number(numbers)); // true

Darüber hinaus stehen viele verschiedene typenspezifische Methoden zur Verfügung wie für das Arbeiten mit Zahlen (greater(), greaterOrEqual(), less(), lessOrEqual(), between() etc.) oder Zeichenketten (unemptyString(), contains() etc.).

All die genannten Methoden liefern immer einen booleschen Wert zurück: true, falls der übergebene Wert vom entsprechenden Typ ist, ansonsten false. Möchte man stattdessen, dass für letzteren Fall gleich einen Fehler geworfen wird, kann man dies durch ein dazwischen gesetztes assert erreichen:

t.assert.number(5);     // Ok
t.assert.number('5'); // Wirft Fehler

Auf diese Weise muss man sich dann nicht mehr selbst darum kümmern, den zurückgegebenen booleschen Wert gegenzuprüfen und einen Fehler zu werfen. Sehr praktisch.

Anwendungsfall: Validierung von Parametern

Was wäre ein Anwendungsfall für die Verwendung von check-types? Zum Beispiel, um innerhalb von Setter-Methoden sicherzustellen, dass jeweils ein gültiger Wert übergeben wurde, sprich die Parameter zu validieren (also quasi dass, was die Präprozessorsprache TypeScript und der statische Typechecker Flow auch auf ihre jeweilige Art und Weise überprüfen). Im folgenden Listing wird beispielsweise sichergestellt, dass die Setter firstName() und lastName() nur Zeichenketten akzeptieren, die Angabe des Wertes 4711 führt dagegen zu einem Fehler:

'use strict';
let t = require('check-types');
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get firstName() {
return this._firstName;
}
set firstName(firstName) {
t.assert.string(firstName);
this._firstName = firstName;
}
get lastName() {
return this._lastName;
}
set lastName(lastName) {
t.assert.string(lastName);
this._lastName = lastName;
}
}
let max = new Person(4711, 'Mustermann'); // Error: Invalid string
Fazit

Typüberprüfungen sind in JavaScript alles andere als einfach. Das Modul check-types nimmt einem vieles an Arbeit ab, ist aufgrund einer sehr verständlichen API relativ einfach zu nutzen und eignet sich beispielsweise gut für die Validierung von Parametern innerhalb von Setter-Methoden.