Schnittstellentypprüfung mit Typescript

292

Diese Frage ist die direkte Analogie zur Überprüfung des Klassentyps mit TypeScript

Ich muss zur Laufzeit herausfinden, ob eine Variable vom Typ eine Schnittstelle implementiert. Hier ist mein Code:

interface A{
    member:string;
}

var a:any={member:"foobar"};

if(a instanceof A) alert(a.member);

Wenn Sie diesen Code auf dem Typoskript-Spielplatz eingeben, wird die letzte Zeile als Fehler markiert: "Der Name A ist im aktuellen Bereich nicht vorhanden." Das stimmt aber nicht, der Name existiert im aktuellen Bereich. Ich kann die Variablendeklaration sogar var a:A={member:"foobar"};ohne Beschwerden des Editors ändern . Nachdem ich im Internet surft und die andere Frage zu SO gefunden habe, habe ich die Benutzeroberfläche in eine Klasse geändert, kann dann aber keine Objektliterale zum Erstellen von Instanzen verwenden.

Ich fragte mich, wie der Typ A so verschwinden könnte, aber ein Blick auf das generierte Javascript erklärt das Problem:

var a = {
    member: "foobar"
};
if(a instanceof A) {
    alert(a.member);
}

Es gibt keine Darstellung von A als Schnittstelle, daher sind keine Laufzeit-Typprüfungen möglich.

Ich verstehe, dass Javascript als dynamische Sprache kein Konzept von Schnittstellen hat. Gibt es eine Möglichkeit, eine Typprüfung für Schnittstellen durchzuführen?

Die automatische Vervollständigung des Typoskript-Spielplatzes zeigt, dass Typoskript sogar eine Methode bietet implements. Wie kann ich es benutzen?

lhk
quelle
4
JavaScript hat kein Konzept für Schnittstellen, aber das liegt nicht daran, dass es eine dynamische Sprache ist. Dies liegt daran, dass Schnittstellen noch nicht implementiert sind.
trusktr
Ja, aber Sie können stattdessen die Klasse interface verwenden. Siehe dieses Beispiel.
Alexey Baranoshnikov
Anscheinend nicht im Jahr 2017. Super relevante Frage jetzt.
DoubleJosh

Antworten:

220

Sie können ohne das instanceofSchlüsselwort erreichen, was Sie wollen, da Sie jetzt benutzerdefinierte Typwächter schreiben können:

interface A{
    member:string;
}

function instanceOfA(object: any): object is A {
    return 'member' in object;
}

var a:any={member:"foobar"};

if (instanceOfA(a)) {
    alert(a.member);
}

Viele Mitglieder

Wenn Sie viele Mitglieder überprüfen müssen, um festzustellen, ob ein Objekt Ihrem Typ entspricht, können Sie stattdessen einen Diskriminator hinzufügen. Das folgende Beispiel ist das grundlegendste und erfordert, dass Sie Ihre eigenen Diskriminatoren verwalten. Sie müssen sich eingehender mit den Mustern befassen, um sicherzustellen, dass Sie doppelte Diskriminatoren vermeiden.

interface A{
    discriminator: 'I-AM-A';
    member:string;
}

function instanceOfA(object: any): object is A {
    return object.discriminator === 'I-AM-A';
}

var a:any = {discriminator: 'I-AM-A', member:"foobar"};

if (instanceOfA(a)) {
    alert(a.member);
}
Fenton
quelle
85
"Es gibt keine Möglichkeit, eine Schnittstelle zur Laufzeit zu überprüfen." Es gibt sie, die sie aus irgendeinem Grund noch nicht implementiert haben.
trusktr
16
Und wenn die Schnittstelle 100 Mitglieder hat, müssen Sie alle 100 überprüfen? Foobar.
Jenny O'Reilly
4
Sie könnten Ihrem Objekt einen Diskriminator hinzufügen, anstatt alle 100 zu überprüfen ...
Fenton
7
Dieses Diskriminator-Paradigma (wie hier geschrieben) unterstützt keine erweiterten Schnittstellen. Eine abgeleitete Schnittstelle würde false zurückgeben, wenn überprüft wird, ob es sich um eine Instanz einer Basisschnittstelle handelt.
Aaron
1
@Fenton Vielleicht weiß ich nicht genug darüber, aber nehmen wir an, Sie hätten eine Schnittstelle B, die die Schnittstelle A erweitert. Sie möchten isInstanceOfA(instantiatedB)true zurückgeben, aber isInstanceOfB(instantiatedA)false. Müsste der Diskriminator von B nicht "I-AM-A" sein, damit letzteres eintritt?
Aaron
86

In TypeScript 1.6 erledigt der benutzerdefinierte Typschutz die Aufgabe.

interface Foo {
    fooProperty: string;
}

interface Bar {
    barProperty: string;
}

function isFoo(object: any): object is Foo {
    return 'fooProperty' in object;
}

let object: Foo | Bar;

if (isFoo(object)) {
    // `object` has type `Foo`.
    object.fooProperty;
} else {
    // `object` has type `Bar`.
    object.barProperty;
}

Und genau wie Joe Yang erwähnt hat: Seit TypeScript 2.0 können Sie sogar den Vorteil von Tagged Union Type nutzen.

interface Foo {
    type: 'foo';
    fooProperty: string;
}

interface Bar {
    type: 'bar';
    barProperty: number;
}

let object: Foo | Bar;

// You will see errors if `strictNullChecks` is enabled.
if (object.type === 'foo') {
    // object has type `Foo`.
    object.fooProperty;
} else {
    // object has type `Bar`.
    object.barProperty;
}

Und es funktioniert auch mit switch.

Vilicvane
quelle
1
Das sieht ziemlich neugierig aus. Anscheinend gibt es eine Art Meta-Information. Warum sollten Sie es mit dieser Type-Guard-Syntax verfügbar machen? Aufgrund welcher Einschränkungen funktioniert "Objekt ist Schnittstelle" neben einer Funktion im Gegensatz zu isinstanceof? Genauer gesagt, könnten Sie "object is interface" direkt in den if-Anweisungen verwenden? Aber auf jeden Fall sehr interessante Syntax, +1 von mir.
lhk
1
@lhk Nein, es gibt keine solche Anweisung, es ist eher ein spezieller Typ, der angibt, wie ein Typ innerhalb bedingter Zweige eingegrenzt werden soll. Aufgrund des "Umfangs" von TypeScript glaube ich, dass es auch in Zukunft keine solche Aussage geben wird. Ein weiterer Unterschied zwischen object is typeund object instanceof classbesteht darin, dass die Eingabe von TypeScript strukturell ist und sich nur um die "Form" kümmert, anstatt darum, woher ein Objekt die Form hat: ein einfaches Objekt oder eine Instanz einer Klasse, es spielt keine Rolle.
Vilicvane
2
Nur um ein Missverständnis zu beseitigen, das diese Antwort hervorrufen kann: Es gibt keine Metainformationen, um den Objekttyp oder seine Schnittstelle zur Laufzeit abzuleiten.
Mostruash
@mostruash Ja, die zweite Hälfte der Antwort funktioniert zur Laufzeit nicht, obwohl sie kompiliert wird.
trusktr
4
Oh, aber dies muss davon ausgehen, dass diese Objekte zur Laufzeit mit einer typeEigenschaft erstellt wurden. In diesem Fall funktioniert es. Dieses Beispiel zeigt diese Tatsache nicht.
Trusktr
40

Typoskript 2.0 führt Tagged Union ein

Typescript 2.0-Funktionen

interface Square {
    kind: "square";
    size: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

interface Circle {
    kind: "circle";
    radius: number;
}

type Shape = Square | Rectangle | Circle;

function area(s: Shape) {
    // In the following switch statement, the type of s is narrowed in each case clause
    // according to the value of the discriminant property, thus allowing the other properties
    // of that variant to be accessed without a type assertion.
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.width * s.height;
        case "circle": return Math.PI * s.radius * s.radius;
    }
}
Joe Yang
quelle
Ich verwende 2.0 Beta, aber getaggte Union funktioniert nicht. <TypeScriptToolsVersion> 2.0 </ TypeScriptToolsVersion>
Makla
Kompiliert mit nächtlichem Build, aber Intellisense funktioniert nicht. Außerdem werden Fehler aufgelistet: Die Eigenschaft width / size / ... ist für Typ 'Square | nicht vorhanden Rechteck | Kreis in case-Anweisung. Aber es kompiliert.
Makla
22
Dies ist wirklich nur ein Diskriminator.
Erik Philips
32

Wie wäre es mit benutzerdefinierten Typwächtern? https://www.typescriptlang.org/docs/handbook/advanced-types.html

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function isFish(pet: Fish | Bird): pet is Fish { //magic happens here
    return (<Fish>pet).swim !== undefined;
}

// Both calls to 'swim' and 'fly' are now okay.

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}
Caleb Macdonald Black
quelle
3
Dies ist meine Lieblingsantwort - ähnlich wie stackoverflow.com/a/33733258/469777, jedoch ohne magische Zeichenfolgen, die aufgrund von Minimierungen brechen können.
Stafford Williams
1
Das hat bei mir aus irgendeinem Grund nicht funktioniert, aber (pet as Fish).swim !== undefined;funktioniert.
CyberMew
18

Es ist jetzt möglich, dass ich gerade eine erweiterte Version des TypeScriptCompilers veröffentlicht habe, die vollständige Reflexionsfunktionen bietet. Sie können Klassen aus ihren Metadatenobjekten instanziieren, Metadaten von Klassenkonstruktoren abrufen und Schnittstellen / Klassen zur Laufzeit überprüfen. Sie können es hier überprüfen

Anwendungsbeispiel:

Erstellen Sie in einer Ihrer Typoskriptdateien eine Schnittstelle und eine Klasse, die sie wie folgt implementiert:

interface MyInterface {
    doSomething(what: string): number;
}

class MyClass implements MyInterface {
    counter = 0;

    doSomething(what: string): number {
        console.log('Doing ' + what);
        return this.counter++;
    }
}

Lassen Sie uns nun einige der Liste der implementierten Schnittstellen drucken.

for (let classInterface of MyClass.getClass().implements) {
    console.log('Implemented interface: ' + classInterface.name)
}

Kompilieren Sie mit Reflect-ts und starten Sie es:

$ node main.js
Implemented interface: MyInterface
Member name: counter - member kind: number
Member name: doSomething - member kind: function

Siehe Reflection.d.ts für InterfaceDetails zum Metatyp.

UPDATE: Ein ausführliches Beispiel finden Sie hier

pcan
quelle
8
downvoted cos Ich fand das dumm, hielt dann aber für eine Sekunde inne, schaute auf deine Github-Seite und sah, dass sie auf dem neuesten Stand und gut dokumentiert war, also upvoted :-) Ich kann es immer noch nicht rechtfertigen, sie selbst zu verwenden implementswollte aber dein Engagement anerkennen und wollte nicht gemein sein :-)
Simon_Weaver
5
Der Hauptzweck dieser Reflexionsfunktionen besteht darin, bessere IoC-Frameworks zu erstellen, wie sie die Java-Welt bereits seit langer Zeit hat (Spring ist das erste und wichtigste). Ich bin fest davon überzeugt, dass TypeScript eines der besten Entwicklungswerkzeuge der Zukunft werden kann und Reflexion eine der Funktionen ist, die es wirklich benötigt.
pcan
5
... ähm, na und, wir müssen diese Compiler- "Verbesserungen" in jeden zukünftigen Build von Typescript einbauen? Dies ist effektiv eine Abzweigung von Typescript, nicht Typescript selbst, oder? Wenn ja, ist dies keine praktikable langfristige Lösung.
Dudewad
1
@dudewad Wie in vielen anderen Themen gesagt, ist dies eine vorübergehende Lösung. Wir warten auf die Erweiterbarkeit des Compilers durch Transformatoren. Weitere Informationen finden Sie im offiziellen TypeScript-Repo. Darüber hinaus haben alle weit verbreiteten stark typisierten Sprachen Reflexion, und ich denke, TypeScript sollte es auch haben. Und wie ich denken viele andere Benutzer so.
pcan
Ja, es ist nicht so, dass ich nicht einverstanden bin - ich will das auch. Nur einen benutzerdefinierten Compiler hochfahren ... bedeutet das nicht, dass der nächste Patch von Typescript portiert werden muss? Wenn Sie es pflegen, dann ein großes Lob. Scheint einfach viel Arbeit zu sein. Nicht klopfen.
Dudewad
9

Wie oben, wo benutzerdefinierte Schutzvorrichtungen verwendet wurden, diesmal jedoch mit einem Pfeilfunktionsprädikat

interface A {
  member:string;
}

const check = (p: any): p is A => p.hasOwnProperty('member');

var foo: any = { member: "foobar" };
if (check(foo))
    alert(foo.member);
Dan Dohotaru
quelle
8

Hier ist eine weitere Option: Das Modul ts-interface-builder bietet ein Build-Time-Tool, das eine TypeScript-Schnittstelle in einen Laufzeitdeskriptor konvertiert, und ts-interface-checker kann prüfen, ob ein Objekt diese Anforderungen erfüllt.

Für das Beispiel von OP:

interface A {
  member: string;
}

Sie würden zuerst ausführen, ts-interface-builderwodurch eine neue übersichtliche Datei mit einem Deskriptor erstellt wird foo-ti.ts, den Sie beispielsweise folgendermaßen verwenden können:

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

A.check({member: "hello"});           // OK
A.check({member: 17});                // Fails with ".member is not a string" 

Sie können eine Einzeiler-Typschutzfunktion erstellen:

function isA(value: any): value is A { return A.test(value); }
DS.
quelle
6

Ich möchte darauf hinweisen, dass TypeScript keinen direkten Mechanismus zum dynamischen Testen bietet, ob ein Objekt eine bestimmte Schnittstelle implementiert.

Stattdessen kann TypeScript-Code mithilfe der JavaScript-Technik prüfen, ob auf dem Objekt eine geeignete Gruppe von Elementen vorhanden ist. Beispielsweise:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}
Daniel Ribeiro
quelle
4
Was ist, wenn Sie eine komplexe Form haben? Sie möchten nicht jede einzelne Eigenschaft in jeder Tiefenebene fest
Tom
@ Tom Ich denke, Sie können (als zweiten Parameter an die Prüffunktion) einen Laufzeitwert oder ein Beispiel / Beispiel übergeben - dh ein Objekt der gewünschten Schnittstelle. Anstelle von hartcodiertem Code schreiben Sie dann ein beliebiges Beispiel für die gewünschte Schnittstelle ... und schreiben einen einmaligen Objektvergleichscode (z. B. for (element in obj) {}), um zu überprüfen, ob die beiden Objekte ähnliche Elemente ähnlicher Typen aufweisen.
ChrisW
3

TypeGuards

interface MyInterfaced {
    x: number
}

function isMyInterfaced(arg: any): arg is MyInterfaced {
    return arg.x !== undefined;
}

if (isMyInterfaced(obj)) {
    (obj as MyInterfaced ).x;
}
Dmitry Matveev
quelle
2
Das "arg is MyInterfaced" ist eine interessante Anmerkung. Was passiert, wenn das fehlschlägt? Sieht aus wie eine Überprüfung der Kompilierungszeitschnittstelle - das wäre genau das, was ich überhaupt wollte. Aber wenn der Compiler die Parameter überprüft, warum überhaupt einen Funktionskörper? Und wenn eine solche Prüfung möglich ist, warum sollte sie dann in eine separate Funktion verschoben werden?
lhk
1
@lhk lesen Sie einfach Typoskript-Dokumentation über Typwächter ... typescriptlang.org/docs/handbook/advanced-types.html
Dmitry Matveev
3

Basierend auf der Antwort von Fenton ist hier meine Implementierung einer Funktion, um zu überprüfen, ob eine gegebene objectdie Schlüssel interfacehat, die sie ganz oder teilweise hat.

Abhängig von Ihrem Anwendungsfall müssen Sie möglicherweise auch die Typen der einzelnen Eigenschaften der Schnittstelle überprüfen. Der folgende Code macht das nicht.

function implementsTKeys<T>(obj: any, keys: (keyof T)[]): obj is T {
    if (!obj || !Array.isArray(keys)) {
        return false;
    }

    const implementKeys = keys.reduce((impl, key) => impl && key in obj, true);

    return implementKeys;
}

Anwendungsbeispiel:

interface A {
    propOfA: string;
    methodOfA: Function;
}

let objectA: any = { propOfA: '' };

// Check if objectA partially implements A
let implementsA = implementsTKeys<A>(objectA, ['propOfA']);

console.log(implementsA); // true

objectA.methodOfA = () => true;

// Check if objectA fully implements A
implementsA = implementsTKeys<A>(objectA, ['propOfA', 'methodOfA']);

console.log(implementsA); // true

objectA = {};

// Check again if objectA fully implements A
implementsA = implementsTKeys<A>(objectA, ['propOfA', 'methodOfA']);

console.log(implementsA); // false, as objectA now is an empty object
Aledpardo
quelle
2
export interface ConfSteps {
    group: string;
    key: string;
    steps: string[];
}
private verify(): void {
    const obj = `{
      "group": "group",
      "key": "key",
      "steps": [],
      "stepsPlus": []
    } `;
    if (this.implementsObject<ConfSteps>(obj, ['group', 'key', 'steps'])) {
      console.log(`Implements ConfSteps: ${obj}`);
    }
  }
private objProperties: Array<string> = [];

private implementsObject<T>(obj: any, keys: (keyof T)[]): boolean {
    JSON.parse(JSON.stringify(obj), (key, value) => {
      this.objProperties.push(key);
    });
    for (const key of keys) {
      if (!this.objProperties.includes(key.toString())) {
        return false;
      }
    }
    this.objProperties = null;
    return true;
  }
Kovács Botond
quelle
1
Während dieser Code die Frage möglicherweise beantwortet, verbessert die Bereitstellung eines zusätzlichen Kontexts darüber, warum und / oder wie dieser Code die Frage beantwortet, ihren langfristigen Wert.
Xiawi
0

Da der Typ zur Laufzeit unbekannt ist, habe ich folgenden Code geschrieben, um das unbekannte Objekt nicht mit einem Typ, sondern mit einem Objekt bekannten Typs zu vergleichen:

  1. Erstellen Sie ein Beispielobjekt des richtigen Typs
  2. Geben Sie an, welche Elemente optional sind
  3. Vergleichen Sie Ihr unbekanntes Objekt gründlich mit diesem Beispielobjekt

Hier ist der (schnittstellenunabhängige) Code, den ich für den Tiefenvergleich verwende:

function assertTypeT<T>(loaded: any, wanted: T, optional?: Set<string>): T {
  // this is called recursively to compare each element
  function assertType(found: any, wanted: any, keyNames?: string): void {
    if (typeof wanted !== typeof found) {
      throw new Error(`assertType expected ${typeof wanted} but found ${typeof found}`);
    }
    switch (typeof wanted) {
      case "boolean":
      case "number":
      case "string":
        return; // primitive value type -- done checking
      case "object":
        break; // more to check
      case "undefined":
      case "symbol":
      case "function":
      default:
        throw new Error(`assertType does not support ${typeof wanted}`);
    }
    if (Array.isArray(wanted)) {
      if (!Array.isArray(found)) {
        throw new Error(`assertType expected an array but found ${found}`);
      }
      if (wanted.length === 1) {
        // assume we want a homogenous array with all elements the same type
        for (const element of found) {
          assertType(element, wanted[0]);
        }
      } else {
        // assume we want a tuple
        if (found.length !== wanted.length) {
          throw new Error(
            `assertType expected tuple length ${wanted.length} found ${found.length}`);
        }
        for (let i = 0; i < wanted.length; ++i) {
          assertType(found[i], wanted[i]);
        }
      }
      return;
    }
    for (const key in wanted) {
      const expectedKey = keyNames ? keyNames + "." + key : key;
      if (typeof found[key] === 'undefined') {
        if (!optional || !optional.has(expectedKey)) {
          throw new Error(`assertType expected key ${expectedKey}`);
        }
      } else {
        assertType(found[key], wanted[key], expectedKey);
      }
    }
  }

  assertType(loaded, wanted);
  return loaded as T;
}

Unten ist ein Beispiel, wie ich es benutze.

In diesem Beispiel erwarte ich, dass JSON ein Array von Tupeln enthält, von denen das zweite Element eine Instanz einer aufgerufenen Schnittstelle ist User(die zwei optionale Elemente enthält).

Durch die Typprüfung von TypeScript wird sichergestellt, dass mein Beispielobjekt korrekt ist. Anschließend überprüft die Funktion assertTypeT, ob das unbekannte (von JSON geladene) Objekt mit dem Beispielobjekt übereinstimmt.

export function loadUsers(): Map<number, User> {
  const found = require("./users.json");
  const sample: [number, User] = [
    49942,
    {
      "name": "ChrisW",
      "email": "[email protected]",
      "gravatarHash": "75bfdecf63c3495489123fe9c0b833e1",
      "profile": {
        "location": "Normandy",
        "aboutMe": "I wrote this!\n\nFurther details are to be supplied ..."
      },
      "favourites": []
    }
  ];
  const optional: Set<string> = new Set<string>(["profile.aboutMe", "profile.location"]);
  const loaded: [number, User][] = assertTypeT(found, [sample], optional);
  return new Map<number, User>(loaded);
}

Sie können eine solche Prüfung bei der Implementierung eines benutzerdefinierten Typschutzes aufrufen.

ChrisW
quelle
0

Sie können einen TypeScript-Typ zur Laufzeit mit ts-validate-type validieren (erfordert jedoch ein Babel-Plugin):

const user = validateType<{ name: string }>(data);
Edbentley
quelle