Verifica del tipo di interfaccia con Typescript


294

Questa domanda è l'analogo diretto al controllo del tipo di classe con TypeScript

Devo scoprire in fase di esecuzione se una variabile di tipo implementa un'interfaccia. Ecco il mio codice:

interface A{
    member:string;
}

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

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

Se si inserisce questo codice nel parco giochi dattiloscritto, l'ultima riga verrà contrassegnata come errore, "Il nome A non esiste nell'ambito corrente". Ma questo non è vero, il nome esiste nell'ambito attuale. Posso anche modificare la dichiarazione delle variabili in var a:A={member:"foobar"};senza lamentele da parte dell'editor. Dopo aver esplorato il Web e aver trovato l'altra domanda su SO, ho cambiato l'interfaccia in una classe, ma non posso usare letterali di oggetti per creare istanze.

Mi chiedevo come il tipo A potesse svanire in quel modo, ma uno sguardo al javascript generato spiega il problema:

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

Non esiste una rappresentazione di A come interfaccia, pertanto non sono possibili controlli del tipo di runtime.

Comprendo che javascript come linguaggio dinamico non ha il concetto di interfacce. Esiste un modo per digitare il controllo delle interfacce?

Il completamento automatico del parco giochi dattiloscritto rivela che dattiloscritto offre persino un metodo implements. Come posso usarlo?


4
JavaScript non ha il concetto di interfacce, ma non è perché è un linguaggio dinamico. È perché le interfacce non sono ancora implementate.
Trusktr,

Sì, ma puoi usare class invece l'interfaccia. Vedi questo esempio
Alexey Baranoshnikov,

Apparentemente non nel 2017. Domanda super pertinente ora.
doppio giovedì

Risposte:


221

Puoi ottenere ciò che desideri senza la instanceofparola chiave poiché puoi scrivere protezioni personalizzate di tipo ora:

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);
}

Molti membri

Se devi controllare molti membri per determinare se un oggetto corrisponde al tuo tipo, puoi invece aggiungere un discriminatore. L'esempio che segue è l'esempio più basilare e richiede di gestire i propri discriminatori ... Avresti bisogno di approfondire gli schemi per assicurarti di evitare discriminatori duplicati.

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);
}

85
"Non c'è modo di verificare un'interfaccia in fase di esecuzione." C'è, non l'hanno ancora implementato per qualsiasi motivo.
Trusktr,

16
E se l'interfaccia ha 100 membri, devi controllare tutti e 100? Foobar.
Jenny O'Reilly,

4
Puoi aggiungere un discriminatore al tuo oggetto piuttosto che controllare tutti i 100 ...
Fenton,

7
questo paradigma discriminatore (come scritto qui) non supporta l'estensione delle interfacce. Un'interfaccia derivata restituisce false se verifica se si tratta di un'istanza di un'interfaccia di base.
Aaron,

1
@Fenton Forse non ne so abbastanza, ma supponiamo che tu abbia un'interfaccia B che estende l'interfaccia A, vorresti isInstanceOfA(instantiatedB)restituire true, ma vorresti isInstanceOfB(instantiatedA)restituire false. Affinché si verifichi quest'ultimo, il discriminatore di B non dovrebbe essere "I-AM-A"?
Aaron,

88

In TypeScript 1.6, la protezione del tipo definita dall'utente farà il lavoro.

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;
}

E proprio come ha detto Joe Yang: da TypeScript 2.0, puoi persino sfruttare il tipo di unione con tag.

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;
}

E funziona switchanche con .


1
Sembra piuttosto curioso. Apparentemente c'è una sorta di meta-informazione disponibile. Perché esporlo con questa sintassi di protezione del tipo. A causa di quali vincoli "object is interface" accanto a una funzione funziona, al contrario di isinstanceof? Più precisamente, potresti usare "object is interface" nelle istruzioni if ​​direttamente? Ma in ogni caso, sintassi molto interessante, +1 da me.
vedi il

1
@lhk No, non esiste una simile affermazione, è più simile a un tipo speciale che dice come dovrebbe essere ristretto un tipo all'interno di rami condizionali. A causa della "portata" di TypeScript, credo che non ci sarà una simile affermazione nemmeno in futuro. Un altro diverso object is typeed object instanceof classè che, digitare TypeScript è strutturale, importa solo la "forma" invece che da dove un oggetto ha preso la forma: un oggetto semplice o un'istanza di una classe, non importa.
vilicvane,

2
Solo per chiarire un malinteso che questa risposta può creare: non ci sono meta informazioni per dedurre il tipo di oggetto o la sua interfaccia durante il runtime.
mostruash,

@mostruash Sì, la seconda metà della risposta non funzionerà in fase di esecuzione anche se viene compilata.
Trusktr,

4
Oh, ma, questo deve presumere che in fase di esecuzione questi oggetti saranno stati creati con una typeproprietà. In quel caso funziona. Questo esempio non mostra questo fatto.
trusktr,

40

il dattiloscritto 2.0 introduce l'unione taggata

Caratteristiche di Typescript 2.0

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;
    }
}

Sto usando 2.0 beta ma l'unione taggata non funziona. <TypeScriptToolsVersion> 2.0 </TypeScriptToolsVersion>
Makla

Compilato con build notturno, ma intellisense non funziona. Elenca anche gli errori: Larghezza / dimensione proprietà / ... non esiste su Tipo 'Quadrato | Rettangolo | Cerchia nella dichiarazione del caso. Ma si compila.
Makla,

23
Questo è davvero solo usando un discriminatore.
Erik Philips,

33

Che ne dite di guardie di tipo definite dall'utente? 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();
}

3
Questa è la mia risposta preferita - simile a stackoverflow.com/a/33733258/469777 ma senza stringhe magiche che potrebbero rompersi a causa di cose come la minificazione.
Stafford Williams,

1
Questo non ha funzionato per me per qualche motivo, ma (pet as Fish).swim !== undefined;ha funzionato.
CyberMew,

18

Ora è possibile, ho appena rilasciato una versione migliorata del TypeScriptcompilatore che offre funzionalità di riflessione complete. È possibile creare un'istanza delle classi dai loro oggetti metadati, recuperare metadati dai costruttori di classi e ispezionare l'interfaccia / le classi in fase di esecuzione. Puoi verificarlo qui

Esempio di utilizzo:

In uno dei tuoi file dattiloscritto, crea un'interfaccia e una classe che la implementa come segue:

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

class MyClass implements MyInterface {
    counter = 0;

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

ora stampiamo un po 'l'elenco delle interfacce implementate.

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

compilare con reflec-ts e avviarlo:

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

Vedi reflection.d.ts per i Interfacedettagli del meta-tipo.

AGGIORNAMENTO: è possibile trovare un esempio funzionante completo qui


8
ho votato perché ho pensato che fosse stupido, ma poi ho fatto una pausa per un secondo, ho guardato la tua pagina di github e ho visto che era aggiornato e ben documentato, quindi invece votato :-) Non riesco ancora a giustificarlo usando me stesso proprio ora implementsma volevo riconoscere il tuo impegno e non volevo essere cattivo :-)
Simon_Weaver,

5
In realtà, lo scopo principale che vedo di queste caratteristiche di riflessione è quello di creare migliori framework IoC come quelli che il mondo Java ha già da molto tempo (la primavera è la prima e la più importante). Sono fermamente convinto che TypeScript possa diventare uno dei migliori strumenti di sviluppo del futuro e la riflessione sia una delle caratteristiche di cui ha davvero bisogno.
pcan,

5
... e allora, dobbiamo inserire questi "miglioramenti" del compilatore in qualsiasi build futura di Typescript? Questo è effettivamente un fork di Typescript, non Typescript stesso, giusto? In tal caso, questa non è una soluzione fattibile a lungo termine.
Dudewad,

1
@dudewad, come detto in molti altri argomenti, questa è una soluzione temporanea. Stiamo aspettando l'estensibilità del compilatore attraverso i trasformatori. Vedere i problemi correlati nel repository TypeScript ufficiale. Inoltre, tutti i linguaggi tipicamente tipicamente adottati hanno una riflessione, e penso che anche TypeScript dovrebbe averlo. E come me, molti altri utenti la pensano così.
pcan,

sì, non è che non sono d'accordo - lo voglio anch'io. Basta girare un compilatore personalizzato ... non significa che la prossima patch di Typescript deve essere trasferita? Se lo stai mantenendo, allora complimenti. Sembra un sacco di lavoro. Non bussare.
Dudewad,

10

come sopra in cui venivano utilizzate protezioni definite dall'utente ma questa volta con un predicato con funzione freccia

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

8

Ecco un'altra opzione: il modulo ts-interface-builder fornisce uno strumento di build-time che converte un'interfaccia TypeScript in un descrittore di runtime e ts-interface-checker può verificare se un oggetto lo soddisfa.

Per l'esempio di OP,

interface A {
  member: string;
}

Dovresti prima eseguire ts-interface-builderche produce un nuovo file conciso con un descrittore, per esempio foo-ti.ts, che puoi usare in questo modo:

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" 

È possibile creare una funzione di protezione del tipo da una riga:

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

6

Vorrei sottolineare che TypeScript non fornisce un meccanismo diretto per testare in modo dinamico se un oggetto implementa una particolare interfaccia.

Al contrario, il codice TypeScript può utilizzare la tecnica JavaScript per verificare se sull'oggetto è presente un set appropriato di membri. Per esempio:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}

4
cosa succede se hai una forma complessa? non vorresti codificare ogni singola proprietà ad ogni livello di profondità
Tom

@Tom Immagino che tu possa passare (come secondo parametro alla funzione checker) un valore di runtime o esempio / esempio - cioè un oggetto dell'interfaccia che desideri. Quindi, invece di un codice hardcoding, scrivi qualsiasi esempio dell'interfaccia che desideri ... e scrivi un codice di confronto di oggetti una tantum (usando ad esempio for (element in obj) {}) per verificare che i due oggetti abbiano elementi simili di tipo simile.
ChrisW,

5

TypeGuards

interface MyInterfaced {
    x: number
}

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

if (isMyInterfaced(obj)) {
    (obj as MyInterfaced ).x;
}

2
"arg is MyInterfaced" è un'annotazione interessante. Cosa succede se fallisce? Sembra un controllo dell'interfaccia del tempo di compilazione, che sarebbe proprio quello che volevo in primo luogo. Ma se il compilatore controlla i parametri, perché avere un corpo di funzione. E se un tale controllo è possibile, perché spostarlo in una funzione separata.
vedi

1
@lhk ha appena letto la documentazione dattiloscritta sulle protezioni dei tipi ... typescriptlang.org/docs/handbook/advanced-types.html
Dmitry Matveev,

3

Sulla base della risposta di Fenton , ecco la mia implementazione di una funzione per verificare se un dato objectha le chiavi che interfaceha, sia completamente che parzialmente.

A seconda del caso d'uso, potrebbe essere necessario controllare anche i tipi di ciascuna proprietà dell'interfaccia. Il codice qui sotto non lo fa.

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;
}

Esempio di utilizzo:

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

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;
  }

1
Mentre questo codice può rispondere alla domanda, fornendo un contesto aggiuntivo riguardo al perché e / o al modo in cui questo codice risponde alla domanda migliora il suo valore a lungo termine.
xiawi,

0

Poiché il tipo è sconosciuto in fase di esecuzione, ho scritto il codice come segue per confrontare l'oggetto sconosciuto, non con un tipo, ma con un oggetto di tipo noto:

  1. Crea un oggetto campione del tipo giusto
  2. Specifica quali dei suoi elementi sono facoltativi
  3. Esegui un confronto approfondito del tuo oggetto sconosciuto con questo oggetto campione

Ecco il codice (interfaccia-agnostico) che uso per il confronto approfondito:

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;
}

Di seguito è riportato un esempio di come lo uso.

In questo esempio mi aspetto che JSON contenga una matrice di tuple, di cui il secondo elemento è un'istanza di un'interfaccia chiamata User (che ha due elementi opzionali).

Il controllo del tipo di TypeScript assicurerà che il mio oggetto campione sia corretto, quindi la funzione assertTypeT verifica che l'oggetto sconosciuto (caricato da JSON) corrisponda all'oggetto campione.

export function loadUsers(): Map<number, User> {
  const found = require("./users.json");
  const sample: [number, User] = [
    49942,
    {
      "name": "ChrisW",
      "email": "example@example.com",
      "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);
}

È possibile richiamare un controllo come questo nell'implementazione di una protezione del tipo definita dall'utente.


0

Puoi convalidare un tipo TypeScript in fase di runtime usando ts-validate-type , in questo modo (richiede comunque un plug-in Babel):

const user = validateType<{ name: string }>(data);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.