Vérification du type d'interface avec Typescript

293

Cette question est l'analogon directe à la vérification de type de classe avec TypeScript

J'ai besoin de savoir à l'exécution si une variable de type any implémente une interface. Voici mon code:

interface A{
    member:string;
}

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

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

Si vous entrez ce code dans le terrain de jeu dactylographié, la dernière ligne sera marquée comme une erreur, "Le nom A n'existe pas dans la portée actuelle". Mais ce n'est pas vrai, le nom existe dans la portée actuelle. Je peux même changer la déclaration de variable en var a:A={member:"foobar"};sans plainte de l'éditeur. Après avoir parcouru le Web et trouvé l'autre question sur SO, j'ai changé l'interface en classe, mais je ne peux plus utiliser les littéraux d'objet pour créer des instances.

Je me demandais comment le type A pouvait disparaître comme ça, mais un coup d'œil au javascript généré explique le problème:

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

Il n'y a pas de représentation de A en tant qu'interface, donc aucune vérification de type d'exécution n'est possible.

Je comprends que javascript en tant que langage dynamique n'a pas de concept d'interfaces. Existe-t-il un moyen de taper la vérification des interfaces?

La saisie semi-automatique du terrain de jeu tapuscrit révèle que le tapuscrit offre même une méthode implements. Comment puis-je l'utiliser?

lhk
la source
4
JavaScript n'a pas de concept d'interfaces, mais ce n'est pas parce qu'il s'agit d'un langage dynamique. C'est parce que les interfaces ne sont pas encore implémentées.
trusktr
Oui, mais vous pouvez utiliser l'interface à la place de la classe. Voir cet exemple.
Alexey Baranoshnikov
Apparemment pas en 2017. Question super pertinente maintenant.
doublejosh

Réponses:

220

Vous pouvez obtenir ce que vous voulez sans le instanceofmot - clé, car vous pouvez maintenant écrire des protecteurs de type personnalisés:

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

Beaucoup de membres

Si vous devez vérifier un grand nombre de membres pour déterminer si un objet correspond à votre type, vous pouvez plutôt ajouter un discriminateur. L'exemple ci-dessous est l'exemple le plus élémentaire et vous oblige à gérer vos propres discriminateurs ... vous devez approfondir les modèles pour vous assurer d'éviter les discriminateurs en double.

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
la source
85
"Il n'y a aucun moyen pour l'exécution de vérifier une interface." Il y a, ils ne l'ont pas encore implémenté pour une raison quelconque.
trusktr
16
Et si l'interface compte 100 membres, vous devez cocher les 100? Foobar.
Jenny O'Reilly
4
Vous pourriez ajouter un discriminateur à votre objet plutôt que de cocher les 100 ...
Fenton
7
ce paradigme discriminateur (comme écrit ici) ne prend pas en charge les interfaces d'extension. Une interface dérivée retournerait false si elle vérifie s'il s'agit d'une instance d'une interface de base.
Aaron
1
@Fenton Peut-être que je n'en sais pas assez à ce sujet, mais supposons que vous disposiez d'une interface B qui étend l'interface A, vous voudriez isInstanceOfA(instantiatedB) retourner vrai, mais vous voudriez isInstanceOfB(instantiatedA)retourner faux. Pour que ce dernier se produise, le discriminateur de B ne devrait-il pas être «I-AM-A»?
Aaron
87

Dans TypeScript 1.6, la protection de type définie par l'utilisateur fera le travail.

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

Et tout comme Joe Yang l'a mentionné: depuis TypeScript 2.0, vous pouvez même tirer parti du type d'union balisé.

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

Et ça marche switchaussi.

vilicvane
la source
1
Cela semble plutôt curieux. Apparemment, il existe une sorte de méta-information. Pourquoi l'exposer avec cette syntaxe de garde de type. En raison de quelles contraintes "objet est interface" à côté d'un travail de fonction, par opposition à isinstanceof? Plus précisément, pourriez-vous utiliser "object is interface" dans les instructions if directement? Mais en tout cas, syntaxe très intéressante, +1 de moi.
lhk
1
@lhk Non, il n'y a pas une telle déclaration, c'est plus comme un type spécial qui indique comment un type doit être restreint à l'intérieur des branches conditionnelles. En raison de la «portée» de TypeScript, je pense qu'il n'y aura pas une telle déclaration même à l'avenir. Une autre différence entre object is typeet object instanceof classest que, le type dans TypeScript est structurel, il ne se soucie que de la "forme" au lieu d'où un objet a obtenu la forme: un objet simple ou une instance d'une classe, cela n'a pas d'importance.
vilicvane
2
Juste pour effacer une idée fausse que cette réponse peut créer: il n'y a pas de méta-informations pour déduire le type d'objet ou son interface pendant l'exécution.
mostruash
@mostruash Oui, la seconde moitié de la réponse ne fonctionnera pas à l'exécution même si elle se compile.
trusktr
4
Oh, mais cela doit supposer qu'au moment de l'exécution, ces objets auront été créés avec une typepropriété. Dans ce cas, cela fonctionne. Cet exemple ne montre pas ce fait.
trusktr
40

tapuscrit 2.0 présente l'union étiquetée

Fonctionnalités de 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;
    }
}
Joe Yang
la source
J'utilise 2.0 beta mais l'union balisée ne fonctionne pas. <TypeScriptToolsVersion> 2.0 </TypeScriptToolsVersion>
Makla
Compilé avec une compilation nocturne, mais intellisense ne fonctionne pas. Il répertorie également les erreurs: la largeur / taille / ... de la propriété n'existe pas sur le type 'Carré | Rectangle | Encerclez dans la déclaration de cas. Mais ça compile.
Makla
23
C'est vraiment juste en utilisant un discriminateur.
Erik Philips
33

Qu'en est-il des protections de type définies par l'utilisateur? 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
la source
3
C'est ma réponse préférée - similaire à stackoverflow.com/a/33733258/469777 mais sans cordes magiques qui peuvent se briser en raison de choses comme la minification.
Stafford Williams
1
Cela n'a pas fonctionné pour moi pour une raison quelconque, mais (pet as Fish).swim !== undefined;cela a fonctionné.
CyberMew
18

C'est maintenant possible, je viens de publier une version améliorée du TypeScriptcompilateur qui offre des capacités de réflexion complètes. Vous pouvez instancier des classes à partir de leurs objets de métadonnées, récupérer des métadonnées à partir de constructeurs de classes et inspecter l'interface / les classes lors de l'exécution. Vous pouvez le vérifier ici

Exemple d'utilisation:

Dans l'un de vos fichiers dactylographiés, créez une interface et une classe qui l'implémente comme suit:

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

class MyClass implements MyInterface {
    counter = 0;

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

imprimons maintenant la liste des interfaces implémentées.

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

compilez avec reflec-ts et lancez-le:

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

Voir réflexion.d.ts pour les Interfacedétails du méta-type.

MISE À JOUR: Vous pouvez trouver un exemple de travail complet ici

pcan
la source
8
parce que j'ai pensé que c'était stupide, mais j'ai fait une pause pendant une seconde, j'ai regardé votre page github et j'ai vu qu'elle était tenue à jour et bien documentée, donc j'ai voté à la place :-) Je ne peux toujours pas justifier de l'utiliser moi-même en ce moment juste pour implementsmais
je
5
En fait, le principal objectif que je vois de ces fonctionnalités de réflexion est de créer de meilleurs cadres IoC comme ceux que le monde Java a déjà depuis longtemps (Spring est le premier et le plus important). Je crois fermement que TypeScript peut devenir l'un des meilleurs outils de développement du futur et la réflexion est l'une des fonctionnalités dont elle a vraiment besoin.
pcan
5
... euh, alors quoi, nous devons intégrer ces "améliorations" du compilateur dans toute future version de Typescript? Il s'agit en fait d'un fork de Typescript, pas de Typescript lui-même, non? Si c'est le cas, ce n'est pas une solution à long terme réalisable.
dudewad
1
@dudewad, comme indiqué dans de nombreux autres sujets, il s'agit d'une solution temporaire. Nous attendons l'extensibilité du compilateur via des transformateurs. Veuillez consulter les problèmes connexes dans le référentiel TypeScript officiel. De plus, tous les langages de type fort largement adoptés ont une réflexion, et je pense que TypeScript devrait également l'avoir. Et comme moi, de nombreux autres utilisateurs le pensent.
pcan
ouais ce n'est pas que je ne suis pas d'accord - je le veux aussi. Juste, faire tourner un compilateur personnalisé ... cela ne signifie-t-il pas que le prochain patch de Typescript doit être porté? Si vous l'entretenez, alors bravo. Cela semble être beaucoup de travail. Ne pas le frapper.
dudewad
10

comme ci - dessus où des protections définies par l'utilisateur ont été utilisées, mais cette fois avec un prédicat de fonction de flèche

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
la source
8

Voici une autre option: le module ts-interface-builder fournit un outil au moment de la construction qui convertit une interface TypeScript en un descripteur d'exécution, et ts-interface-checker peut vérifier si un objet le satisfait.

Pour l'exemple d'OP,

interface A {
  member: string;
}

Vous devez d'abord exécuter ts-interface-builderce qui produit un nouveau fichier concis avec un descripteur, par exemple foo-ti.ts, que vous pouvez utiliser comme ceci:

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" 

Vous pouvez créer une fonction de protection de type à une ligne:

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

Je voudrais souligner que TypeScript ne fournit pas de mécanisme direct pour tester dynamiquement si un objet implémente une interface particulière.

Au lieu de cela, le code TypeScript peut utiliser la technique JavaScript pour vérifier si un ensemble approprié de membres est présent sur l'objet. Par exemple:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}
Daniel Ribeiro
la source
4
Et si vous avez une forme complexe? vous ne voudriez pas coder en dur chaque propriété à chaque niveau de profondeur
Tom
@Tom Je suppose que vous pouvez passer (comme deuxième paramètre à la fonction de vérificateur) une valeur d'exécution ou un exemple / exemple - c'est-à-dire un objet de l'interface que vous voulez. Ensuite, au lieu de coder en dur, vous écrivez n'importe quel exemple de l'interface que vous souhaitez ... et écrivez un code de comparaison d'objet unique (en utilisant, par exemple for (element in obj) {}) pour vérifier que les deux objets ont les éléments similaires de types similaires.
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;
}
Dmitry Matveev
la source
2
"arg is MyInterfaced" est une annotation intéressante. Que se passe-t-il si cela échoue? On dirait une vérification de l'interface de compilation - ce qui serait exactement ce que je voulais en premier lieu. Mais si le compilateur vérifie les paramètres, pourquoi avoir un corps de fonction? Et si une telle vérification est possible, pourquoi la déplacer vers une fonction distincte.
lhk
1
@lhk vient de lire la documentation dactylographiée sur les gardes de type ... typescriptlang.org/docs/handbook/advanced-types.html
Dmitry Matveev
3

Sur la base de la réponse de Fenton , voici mon implémentation d'une fonction pour vérifier si un donné objecta les clés uninterface a, à la fois totalement ou partiellement.

Selon votre cas d'utilisation, vous devrez peut-être également vérifier les types de chacune des propriétés de l'interface. Le code ci-dessous ne fait pas cela.

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

Exemple d'utilisation:

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
la source
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
la source
1
Bien que ce code puisse répondre à la question, fournir un contexte supplémentaire concernant pourquoi et / ou comment ce code répond à la question améliore sa valeur à long terme.
xiawi
0

Parce que le type est inconnu au moment de l'exécution, j'ai écrit le code comme suit pour comparer l'objet inconnu, non pas contre un type, mais contre un objet de type connu:

  1. Créer un exemple d'objet du bon type
  2. Spécifiez lesquels de ses éléments sont facultatifs
  3. Faites une comparaison approfondie de votre objet inconnu avec cet exemple d'objet

Voici le code (interface-agnostique) que j'utilise pour la comparaison approfondie:

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

Voici un exemple de la façon dont je l'utilise.

Dans cet exemple, je m'attends à ce que le JSON contienne un tableau de tuples, dont le deuxième élément est une instance d'une interface appelée User (qui a deux éléments facultatifs).

La vérification de type de TypeScript garantira que mon exemple d'objet est correct, puis la fonction assertTypeT vérifie que l'objet inconnu (chargé à partir de JSON) correspond à l'exemple d'objet.

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

Vous pouvez invoquer une vérification comme celle-ci dans l'implémentation d'un type de garde défini par l'utilisateur.

ChrisW
la source
0

Vous pouvez valider un type TypeScript lors de l'exécution en utilisant ts-validate-type , comme cela (nécessite un plugin Babel cependant):

const user = validateType<{ name: string }>(data);
edbentley
la source