Überprüfen Sie, ob ein Objekt zur Laufzeit eine Schnittstelle mit TypeScript implementiert

75

Ich lade zur Laufzeit eine JSON-Konfigurationsdatei und verwende eine Schnittstelle, um die erwartete Struktur zu definieren:

interface EngineConfig {
    pathplanner?: PathPlannerConfig;
    debug?: DebugConfig;
    ...
}

interface PathPlannerConfig {
    nbMaxIter?: number;
    nbIterPerChunk?: number;
    heuristic?: string;
}

interface DebugConfig {
    logLevel?: number;
}

...

Dies macht es bequem, auf die verschiedenen Eigenschaften zuzugreifen, da ich Autovervollständigungen usw. verwenden kann.

Frage: Gibt es eine Möglichkeit, mit dieser Deklaration die Richtigkeit der von mir geladenen Datei zu überprüfen? dh dass ich keine unerwarteten Eigenschaften habe?

MasterScrat
quelle

Antworten:

36

Es gibt einen Weg, aber Sie müssen ihn selbst implementieren. Es heißt "User Defined Type Guard" und sieht folgendermaßen aus:

interface Test {
    prop: number;
}

function isTest(arg: any): arg is Test {
    return arg && arg.prop && typeof(arg.prop) == 'number';
}

Natürlich liegt die tatsächliche Implementierung der isTestFunktion ganz bei Ihnen, aber das Gute daran ist, dass es sich um eine tatsächliche Funktion handelt, was bedeutet, dass sie testbar ist.

Jetzt würden Sie zur Laufzeit isTest()überprüfen, ob ein Objekt eine Schnittstelle respektiert. Zur Kompilierungszeit nimmt das Typoskript die Wache auf und behandelt die spätere Verwendung wie erwartet, dh:

let a:any = { prop: 5 };

a.x; //ok because here a is of type any

if (isTest(a)) {
    a.x; //error because here a is of type Test
}

Ausführlichere Erklärungen finden Sie hier: https://basarat.gitbook.io/typescript/type-system/typeguard

Teodor Sandu
quelle
13
Interessant. Sieht aus wie etwas, das leicht automatisch generiert werden könnte.
MasterScrat
1
Ja, es könnte automatisiert werden, und zwar leicht für häufige Fälle. Der benutzerdefinierte Schutz kann jedoch bestimmte Aufgaben ausführen, z. B. die Länge eines Arrays überprüfen oder eine Zeichenfolge anhand eines regulären Ausdrucks validieren. Anmerkungen pro Feld würden helfen, aber dann würde ich denken, dass es jetzt eher eine Klasse als eine Schnittstelle sein sollte.
Teodor Sandu
11
@RichardForrester meine Frage ist "Gibt es eine Möglichkeit, die Typdeklaration zu verwenden, um die Richtigkeit des Objekts zu überprüfen". Diese Antwort verwendet nicht die Typdeklaration. Stattdessen müssen Tests geschrieben werden, die mit der Typdeklaration vollständig redundant sind. Genau das möchte ich vermeiden.
MasterScrat
23

Nein.

Derzeit werden Typen nur während der Entwicklungs- und Kompilierungszeit verwendet. Die Typinformationen werden in keiner Weise in den kompilierten JavaScript-Code übersetzt.

Von https://stackoverflow.com/a/16016688/318557 , wie von @JasonEvans hervorgehoben

Seit Juni 2015 gibt es im TypeScript-Repo ein offenes Problem dazu: https://github.com/microsoft/TypeScript/issues/3628

MasterScrat
quelle
1
Dies ist nicht mehr korrekt. Der Typoskript-Compiler kennzeichnet experimentalDecoratorsund emitDecoratorMetadataermöglicht das Aufzeichnen von Typinformationen. Siehe meine Antwort für eine Bibliothek, die ich geschrieben habe und die diese Informationen zur Laufzeit verwendet.
Moshe Gottlieb
1
Diese Antwort ist für die gestellte Frage genau richtig, aber Googler sollten beachten, dass es gute Lösungen gibt. Betrachten Sie Alexys Vorschlag zur Klassenvalidierung (meine Präferenz), DSs Empfehlungen von Interface-Buildern von Drittanbietern und Teodors Vorschlag von Typwächtern.
Jtschoonhoven
20

Hier ist eine andere Alternative, speziell dafür:

ts-interface-builder ist ein Tool, das Sie zur Erstellungszeit in Ihrer TypeScript-Datei ausführen foo.ts(z foo-ti.ts. B. ), um Laufzeitbeschreibungen zu erstellen (z . B. ).

ts-interface-checker verwendet diese, um Objekte zur Laufzeit zu validieren. Z.B

import {createCheckers} from 'ts-interface-checker';
import fooDesc from 'foo-ti.ts';
const checkers = createCheckers(fooDesc);

checkers.EngineConfig.check(someObject);   // Succeeds or throws an informative error
checkers.PathPlannerConfig.check(someObject);

Mit der strictCheck()Methode können Sie sicherstellen, dass keine unbekannten Eigenschaften vorhanden sind.

DS.
quelle
12

Hier ist ein guter Weg. Sie können eine TypeScript-Schnittstelle mithilfe des Typenskript- JSON-Schemas in ein JSON-Schema konvertieren , z

typescript-json-schema --required --noExtraProps \
  -o YOUR_SCHEMA.json YOUR_CODE.ts YOUR_INTERFACE_NAME

Überprüfen Sie dann die Daten zur Laufzeit mit einem JSON-Schema-Validator wie ajv , z

const fs = require('fs');
const Ajv = require('ajv');

// Load schema
const schema = JSON.parse(fs.readFileSync('YOUR_SCHEMA.json', {encoding:"utf8"}));
const ajv = new Ajv();
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json'));
var validator = ajv.compile(schema);

if (!validator({"hello": "world"})) {
  console.log(validator.errors);
}
DS.
quelle
Beide Pakete sind fantastisch - hat mir so viel Arbeit erspart! Verwenden der generierten Schemata in Verbindung mit ajv
danday74
7

Ich vermute, dass TypeScript (mit Bedacht) das Curly-Gesetz einhält und Typescript ein Transpiler und kein Objektvalidator ist. Ich denke jedoch auch, dass Typoskript-Schnittstellen zu einer miesen Objektvalidierung führen würden, da Schnittstellen ein (wunderbar) begrenztes Vokabular haben und nicht anhand von Formen validieren können, die andere Programmierer zur Unterscheidung von Objekten verwenden können, wie z. B. Array-Länge, Anzahl der Eigenschaften, Mustereigenschaften usw.

Wenn ich Objekte aus Nicht-Typoskript-Code verwende, verwende ich ein JSONSchema- Validierungspaket wie AJV zur Laufzeitvalidierung und einen .d.ts-Dateigenerator (wie DTSgenerator oder DTS-Generator ), um TypeScript-Typdefinitionen aus my zu kompilieren JSONshcema.

Die größte Einschränkung besteht darin, dass JSON-Schemata Formen beschreiben können, die nicht durch Typoskript unterschieden werden können (z. B. patternProperties) ). Es handelt sich also nicht um eine Eins-zu-Eins-Übersetzung vom JSON-Schema in .t.ds, und Sie müssen möglicherweise etwas Handarbeit leisten Bearbeiten von generierten .d.ts-Dateien bei Verwendung solcher JSON-Schemata.

Da andere Programmierer möglicherweise Eigenschaften wie die Array-Länge verwenden, um auf den Objekttyp zu schließen, habe ich die Gewohnheit, Typen zu unterscheiden, die vom TypeScript-Compiler durch Aufzählungen verwirrt werden könnten, um zu verhindern, dass der Transpiler die Verwendung eines Typs anstelle des Typs akzeptiert andere, wie so:

[MyTypes.yaml]

definitions: 
    type-A: 
        type: object
        properties:
            type:
                enum:
                - A
            foo: 
                type: array
                item: string
                maxLength: 2
    type-B: 
        type: object
        properties:
            type:
                enum:
                - B
            foo: 
                type: array
                item: string
                minLength: 3
        items: number

Welches erzeugt eine .d.tsDatei wie folgt:

[MyTypes.d.ts]

interface typeA{
    type: "A";
    foo: string[];
}

interface typeB{
    type: "B";
    foo: string[];
}
Jthorpe
quelle
4

Ja. Sie können diese Überprüfung zur Laufzeit durchführen, indem Sie eine erweiterte Version des TypeScript-Compilers verwenden, die ich vor einiger Zeit veröffentlicht habe. Sie können Folgendes tun:

export interface Person {
    name: string;
    surname: string;
    age: number;
}

let personOk = { name: "John", surname: "Doe", age: 36 };
let personNotOk = { name: 22, age: "x" };

// YES. Now you CAN use an interface as a type reference object.
console.log("isValid(personOk):  " + isValid(personOk, Person) + "\n");
console.log("isValid(personNotOk):  " + isValid(personNotOk, Person) + "\n");

und das ist die Ausgabe:

isValid(personOk):  true

Field name should be string but it is number
isValid(personNotOk):  false

Bitte beachten Sie, dass die isValidFunktion rekursiv funktioniert , sodass Sie sie auch zum Überprüfen verschachtelter Objekte verwenden können. Das vollständige Arbeitsbeispiel finden Sie hier

pcan
quelle
1
Ordentlich, jede Hoffnung, so etwas in TypeScript zu integrieren. Ich dachte, das war etwas, was AtScript tat und was Angular 2 brauchte.
Jpierson
5
Der offizielle TypeScript-Compiler deckt keine Reflexion ab (und wird dies wahrscheinlich auch nie tun), da er als "außerhalb des Geltungsbereichs" angegeben ist. Für mich war es eine 10-tägige Entwicklungsarbeit, und ich bin nicht im Kernteam: Ich musste viele Dinge lernen, bevor ich effektiv wurde. Eines der Mitglieder des TypeScript-Teams könnte dies in einer Woche oder weniger erreichen. Mit wenigen Worten: Die Reflection-Implementierung in TypeScript ist nicht unmöglich oder zu schwierig .
pcan
Wie kann ich diese isValidFunktion nutzen? Soweit ich sehen kann, wird es nicht aus Ihrem Paket exportiert. Es wird in den Beispielen aus der Validator-Datei exportiert, aber das ist nicht im Paket enthalten. Wie kann ich es am besten in meinen Code aufnehmen?
S ..
1
@S .. die isValidFunktion ist nur eine Demonstration der Reflexionsfähigkeiten; Wenn Sie eine vollständige Typprüfung erstellen möchten, sollten Sie dies selbst tun oder andere ähnliche Projekte verwenden (diese können jedoch derzeit keine Reflexion nutzen). Der Hauptpunkt hierbei ist nicht die Typprüfung, sondern die Fähigkeit des Compilers, zur Laufzeit auf eine Schnittstelle zu verweisen. Bitte beachten Sie, dass sich das Reflect-Ts-Projekt derzeit im Standby-Modus befindet, da ich darauf warte, dass die Transformator-Erweiterbarkeits-API vom Compilerkern entkoppelt wird.
pcan
3

Ja, es gibt eine Bibliothek, die dies tut https://github.com/gcanti/io-ts

Die Idee ist einfach: Lassen Sie einfache Überprüfungen für Eigenschaften zu komplexeren Überprüfungen für Objekte zusammensetzen

Dreizack D'Gao
quelle
1

Sie können verwenden Klassenvalidierung verwenden

  1. Schnittstelle zur Klasse ersetzen.
    Klasse Katze {
        @IsNotEmpty () name: string;
    }}

    // Statisches Tippen ist Arbeit !!!
    const cat: Cat = { 
        Name: "Barsik"
    };

  1. Validierungsfunktion erstellen. Beispiel:
    {validateSync} aus "class-validator" importieren;

    Typ Daten = {
        [key: string]: any;
    };

    // Neue Klasseninstanz erstellen, füllen und über "Klassenvalidator" validieren
    export const validate = <D erweitert Daten, C erweitert {new (): D}>
      (Daten: D, classTemplate: C): boolean => {
        const instanceClass = new classTemplate ();
        Object.keys (Daten) .forEach ((Schlüssel) => {
            instanceClass [Schlüssel] = Daten [Schlüssel];
        });
        return! validateSync (instanceClass) .length;
    }}

  1. Verwenden Sie class als Schnittstelle für die statische Typisierung und class für die Validierung
    if (validieren (Katze, Katze)) {
      // IN ORDNUNG
    } else {
      // ERROR
    }}

Alexey Baranoshnikov
quelle
1

Mir ist klar, dass diese Frage alt ist, aber ich habe gerade meinen eigenen Validator für JSON-Objekte und Typoskript geschrieben, und zwar genau zu diesem Zweck unter Verwendung von Dekoratoren.
Hier verfügbar: ts-json-object .
Typescript hat sich seit dieser Frage etwas weiterentwickelt und verfügt nun über experimentelle Funktionen, mit denen Typinformationen für die spätere Verwendung aufgezeichnet werden können.
Das folgende Beispiel validiert @requiredund @optionalEigenschaften, validiert aber auch ihren Typ, obwohl der Typ in der Validierungsnotation nicht erwähnt wird.

Beispiel:

import {JSONObject,required,optional,lt,gte} from 'ts-json-object'

class Person extends JSONObject {
    @required // required
    name: string
    @optional // optional!
    @lt(150) // less than 150
    @gte(0) // Greater or equal to 0
    age?: number
}

let person = new Person({
 name: 'Joe'
}) // Ok
let person = new Person({
}) // Will throw a TypeError, because name is required
let person = new Person({
 name: 123
}) // Will throw a TypeError, because name must be a string

Hat viele andere Funktionen wie benutzerdefinierte Validierungen usw.

Moshe Gottlieb
quelle
0

Ich weiß nicht, wie Ihre Konfigurationsdatei aussieht, aber am offensichtlichsten wäre die JSON-Datei, obwohl ich das JSON-Schema verwenden würde, um zu überprüfen, ob die Datei zum Schema passt oder nicht.

Hier ist die Dokumentation zu json schema v4: http://json-schema.org/documentation.html

Und eines der Beispiele, wie Sie es testen können: https://github.com/fge/json-schema-validator

Natürlich müssen Sie Ihr Schema basierend auf Schnittstellen schreiben, aber Sie können sie nicht direkt verwenden.

Maciej Kwas
quelle
1
Ja, es ist JSON, aber mein Ziel ist es, es zur Laufzeit mithilfe der bereits vorhandenen Schnittstelle (oder einer anderen Lösung, bei der ich die Konfigurationsstruktur nicht zweimal schreiben muss) zu validieren
MasterScrat
1
Sie können diese Datei zur Laufzeit validieren (laden Sie sie mit einer Ajax-Anfrage und prüfen Sie, ob sie gültig ist oder nicht). Aber lassen Sie mich eines erklären: Die Schnittstelle ist eine Art Schema für Ihr Javascript-Objekt, sie wird nur zur Kompilierungszeit verwendet. Es fehlen jedoch zu viele Informationen, um als Validator verwendet zu werden. Wie würden Sie beispielsweise in Ihre Schnittstelle schreiben, dass ein Array mindestens drei Aufzählungswerte enthalten soll? Sie müssen ein Schema schreiben, um flexibler zu sehen, wie Ihr Objekt aussehen muss. Es gibt einige Generatoren online, die ein Schema basierend auf Ihrer JSON-
Maciej Kwas