Verificación de tipo de interfaz con mecanografiado

293

Esta pregunta es el análogo directo a la verificación de tipo de clase con TypeScript

Necesito averiguar en tiempo de ejecución si una variable de tipo implementa una interfaz. Aquí está mi código:

interface A{
    member:string;
}

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

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

Si ingresa este código en el campo de juegos mecanografiado, la última línea se marcará como un error, "El nombre A no existe en el ámbito actual". Pero eso no es cierto, el nombre existe en el ámbito actual. Incluso puedo cambiar la declaración de la variable var a:A={member:"foobar"};sin quejas del editor. Después de navegar por la web y encontrar la otra pregunta en SO, cambié la interfaz a una clase pero luego no puedo usar literales de objeto para crear instancias.

Me preguntaba cómo el tipo A podría desaparecer así, pero un vistazo al JavaScript generado explica el problema:

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

No hay representación de A como interfaz, por lo tanto, no es posible realizar verificaciones de tipo de tiempo de ejecución.

Entiendo que javascript como lenguaje dinámico no tiene concepto de interfaces. ¿Hay alguna forma de escribir check para interfaces?

El autocompletado del campo de escritura mecanografiado revela que el mecanografiado incluso ofrece un método implements. Como puedo usar lo ?

lhk
fuente
44
JavaScript no tiene un concepto de interfaces, pero eso no se debe a que es un lenguaje dinámico. Es porque las interfaces aún no están implementadas.
trusktr
Sí, pero puede usar la clase en lugar de la interfaz. Ver este ejemplo.
Alexey Baranoshnikov
Aparentemente no en 2017. Pregunta súper relevante ahora.
doublejosh

Respuestas:

220

Puede lograr lo que desea sin la instanceofpalabra clave, ya que puede escribir protectores de tipo personalizados ahora:

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

Muchos miembros

Si necesita verificar muchos miembros para determinar si un objeto coincide con su tipo, puede agregar un discriminador. El siguiente es el ejemplo más básico, y requiere que maneje sus propios discriminadores ... necesitaría profundizar en los patrones para asegurarse de evitar discriminadores duplicados.

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
fuente
85
"No hay forma de verificar el tiempo de ejecución de una interfaz". Hay, simplemente no lo han implementado todavía por cualquier razón.
trusktr
16
¿Y si la interfaz tiene 100 miembros, debe verificar los 100? Foobar
Jenny O'Reilly
44
Podría agregar un discriminador a su objeto en lugar de verificar los 100 ...
Fenton
77
este paradigma discriminador (como está escrito aquí) no admite interfaces extendidas. Una interfaz derivada devolvería falso si se verifica si es una instancia de una interfaz base.
Aaron
1
@Fenton Tal vez no sé lo suficiente sobre esto, pero supongamos que tiene una interfaz B que extiende la interfaz A, desearía isInstanceOfA(instantiatedB)devolver verdadero, pero desearía isInstanceOfB(instantiatedA)devolver falso. Para que esto último ocurra, ¿no debería el discriminador de B no ser 'I-AM-A'?
Aaron
87

En TypeScript 1.6, la protección de tipo definida por el usuario hará el trabajo.

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

Y tal como lo mencionó Joe Yang: desde TypeScript 2.0, incluso puede aprovechar el tipo de unión etiquetado.

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

Y también funciona con switch.

vilicvane
fuente
1
Esto se ve bastante curioso. Aparentemente hay algún tipo de metainformación disponible. ¿Por qué exponerlo con esta sintaxis de protección de tipo? ¿Debido a qué restricciones funciona "objeto es interfaz" al lado de una función, en lugar de isinstanceof? Más precisamente, ¿podría usar "object is interface" en las declaraciones if directamente? Pero en cualquier caso, sintaxis muy interesante, +1 de mi parte.
lhk
1
@lhk No, no existe una declaración de este tipo, es más como un tipo especial que dice cómo se debe reducir un tipo dentro de las ramas condicionales. Debido al "alcance" de TypeScript, creo que no habrá tal declaración incluso en el futuro. Otra diferencia entre object is typey object instanceof classes que, escribir TypeScript es estructural, solo le importa la "forma" en lugar de dónde obtuvo un objeto la forma: un objeto plano o una instancia de una clase, no importa.
vilicvane
2
Solo para aclarar un error, esta respuesta puede crear: no hay metainformación para deducir el tipo de objeto o su interfaz durante el tiempo de ejecución.
mostruash
@mostruash Sí, la segunda mitad de la respuesta no funcionará en tiempo de ejecución a pesar de que se compila.
trusktr
44
Ah, pero, esto debe suponer que en tiempo de ejecución estos objetos se habrán creado con una typepropiedad. En ese caso funciona. Ese ejemplo no muestra este hecho.
trusktr
40

mecanografiado 2.0 introducir unión etiquetada

Características de Typecript 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
fuente
Estoy usando 2.0 beta pero la unión etiquetada no funciona. <TypeScriptToolsVersion> 2.0 </TypeScriptToolsVersion>
Makla
Compilado con compilación nocturna, pero intellisense no funciona. También enumera errores: El ancho / tamaño de la propiedad / ... no existe en el Tipo 'Cuadrado | Rectángulo | Círculo en caso de declaración. Pero se compila.
Makla
23
Esto realmente solo está usando un discriminador.
Erik Philips
33

¿Qué hay de los protectores de tipo definidos por el usuario? 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
fuente
3
Esta es mi respuesta favorita, similar a stackoverflow.com/a/33733258/469777 pero sin cadenas mágicas que pueden romperse debido a cosas como la minificación.
Stafford Williams
1
Esto no funcionó para mí por alguna razón, pero lo (pet as Fish).swim !== undefined;hizo.
CyberMew
18

Ahora es posible, acabo de lanzar una versión mejorada del TypeScriptcompilador que proporciona capacidades de reflexión completas. Puede crear instancias de clases de sus objetos de metadatos, recuperar metadatos de constructores de clases e inspeccionar interfaces / clases en tiempo de ejecución. Puedes verlo aquí

Ejemplo de uso:

En uno de sus archivos de mecanografía, cree una interfaz y una clase que la implemente de la siguiente manera:

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

class MyClass implements MyInterface {
    counter = 0;

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

ahora imprimamos la lista de interfaces implementadas.

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

compile con reflec-ts y ejecútelo:

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

Ver reflection.d.ts para Interfacedetalles de metatipo.

ACTUALIZACIÓN: puede encontrar un ejemplo de trabajo completo aquí

pcan
fuente
8
rechazado porque pensé que esto era estúpido, pero luego me detuve por un segundo, miré su página de github y vi que estaba actualizado y bien documentado, así que voté en su lugar :-) Todavía no puedo justificar usarlo ahora mismo solo por implementspero quería reconocer su compromiso y no quería ser malo :-)
Simon_Weaver
55
En realidad, el propósito principal que veo de estas características de reflexión es crear mejores marcos de IoC como los que el mundo Java ya tiene desde hace mucho tiempo (Spring es el primero y el más importante). Creo firmemente que TypeScript puede convertirse en una de las mejores herramientas de desarrollo del futuro y la reflexión es una de las características que realmente necesita.
pcan
55
... eh, ¿y qué, tenemos que incluir estas "mejoras" del compilador en cualquier compilación futura de Typecript? Esto es efectivamente una bifurcación de mecanografiado, no mecanografiado, ¿verdad? Si es así, esta no es una solución factible a largo plazo.
dudewad
1
@dudewad como se dijo en muchos otros temas, esta es una solución temporal. Estamos esperando la extensibilidad del compilador a través de transformadores. Consulte los problemas relacionados en el repositorio oficial de TypeScript. Además, todos los lenguajes de tipo fuerte ampliamente adoptados tienen reflejo, y creo que TypeScript también debería tenerlo. Y como yo, muchos otros usuarios piensan que sí.
pcan
Sí, no es que no esté de acuerdo, también quiero esto. Simplemente, poner en marcha un compilador personalizado ... ¿no significa que el próximo parche de Typecript necesita ser portado? Si lo estás manteniendo, felicitaciones. Simplemente parece mucho trabajo. No lo golpeo.
dudewad
10

igual que el anterior donde se usaron guardias definidos por el usuario pero esta vez con un predicado de función de flecha

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
fuente
8

Aquí hay otra opción: el módulo ts-interface-builder proporciona una herramienta de tiempo de construcción que convierte una interfaz TypeScript en un descriptor de tiempo de ejecución, y ts-interface-checker puede verificar si un objeto lo satisface.

Para el ejemplo de OP,

interface A {
  member: string;
}

Primero ejecutaría lo ts-interface-builderque produce un nuevo archivo conciso con un descriptor, digamos foo-ti.ts, que puede usar así:

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" 

Puede crear una función de protección de tipo de una línea:

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

Me gustaría señalar que TypeScript no proporciona un mecanismo directo para probar dinámicamente si un objeto implementa una interfaz particular.

En cambio, el código TypeScript puede usar la técnica de JavaScript para verificar si un conjunto apropiado de miembros está presente en el objeto. Por ejemplo:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}
Daniel Ribeiro
fuente
44
¿Qué pasa si tienes una forma compleja? no querrás codificar cada propiedad en cada nivel de profundidad
Tom
@ Tom Creo que puede pasar (como un segundo parámetro a la función de verificación) un valor de tiempo de ejecución o ejemplo / ejemplo, es decir, un objeto de la interfaz que desee. Luego, en lugar del código de codificación rígida, escriba cualquier ejemplo de la interfaz que desee ... y escriba un código de comparación de objetos único (usando, por ejemplo for (element in obj) {}) para verificar que los dos objetos tengan elementos similares de tipos similares.
ChrisW
5

Tipo Guardias

interface MyInterfaced {
    x: number
}

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

if (isMyInterfaced(obj)) {
    (obj as MyInterfaced ).x;
}
Dmitry Matveev
fuente
2
"arg is MyInterfaced" es una anotación interesante. ¿Qué pasa si eso falla? Parece una comprobación de la interfaz de tiempo de compilación, que sería justo lo que quería en primer lugar. Pero si el compilador verifica los parámetros, ¿por qué tener un cuerpo de función? Y si tal verificación es posible, ¿por qué moverla a una función separada?
lhk
1
@lhk acaba de leer la documentación mecanografiada sobre los protectores de tipo ... typescriptlang.org/docs/handbook/advanced-types.html
Dmitry Matveev
3

Basado en la respuesta de Fenton , aquí está mi implementación de una función para verificar si un dado objecttiene las claves y interfacetiene, total o parcialmente.

Dependiendo de su caso de uso, es posible que también deba verificar los tipos de cada una de las propiedades de la interfaz. El siguiente código no hace eso.

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

Ejemplo de uso:

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
fuente
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
fuente
1
Si bien este código puede responder a la pregunta, proporcionar un contexto adicional con respecto a por qué y / o cómo este código responde a la pregunta mejora su valor a largo plazo.
xiawi
0

Debido a que el tipo es desconocido en tiempo de ejecución, escribí el siguiente código para comparar el objeto desconocido, no contra un tipo, sino contra un objeto de tipo conocido:

  1. Crear un objeto de muestra del tipo correcto
  2. Especifica cuáles de sus elementos son opcionales
  3. Haga una comparación profunda de su objeto desconocido con este objeto de muestra

Aquí está el código (interfaz agnóstico) que uso para la comparación profunda:

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

A continuación se muestra un ejemplo de cómo lo uso.

En este ejemplo, espero que el JSON contenga una matriz de tuplas, de las cuales el segundo elemento es una instancia de una interfaz llamada User (que tiene dos elementos opcionales).

La verificación de tipos de TypeScript asegurará que mi objeto de muestra sea correcto, luego la función afirmarTypeT verifica que el objeto desconocido (cargado desde JSON) coincida con el objeto de muestra.

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

Podría invocar una verificación como esta en la implementación de una protección de tipo definida por el usuario.

ChrisW
fuente
0

Puede validar un tipo TypeScript en tiempo de ejecución utilizando ts-validate-type , de esta manera (aunque requiere un complemento de Babel):

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