Verificação do tipo de interface com o Typecript

292

Esta questão é o analogon direto da verificação de tipo de classe com o TypeScript

Preciso descobrir em tempo de execução se uma variável do tipo any implementa uma interface. Aqui está o meu código:

interface A{
    member:string;
}

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

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

Se você digitar esse código no parque de texto datilografado, a última linha será marcada como um erro "O nome A não existe no escopo atual". Mas isso não é verdade, o nome existe no escopo atual. Posso até alterar a declaração da variável para var a:A={member:"foobar"};sem queixas do editor. Depois de navegar na web e encontrar a outra pergunta no SO, mudei a interface para uma classe, mas não consigo usar literais de objeto para criar instâncias.

Gostaria de saber como o tipo A poderia desaparecer assim, mas uma olhada no javascript gerado explica o problema:

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

Não há representação de A como uma interface, portanto, nenhuma verificação de tipo de tempo de execução é possível.

Eu entendo que o javascript como uma linguagem dinâmica não tem conceito de interfaces. Existe alguma maneira de digitar check para interfaces?

O preenchimento automático do playground de texto datilografado revela que o texto datilografado ainda oferece um método implements. Como posso usá-lo?

lhk
fonte
4
JavaScript não tem conceito de interfaces, mas não é por ser uma linguagem dinâmica. É porque as interfaces ainda não foram implementadas.
trusktr
Sim, mas você pode usar a interface de classe. Veja este exemplo.
Alexey Baranoshnikov
Aparentemente, não em 2017. Pergunta super relevante agora.
doublejosh 26/03

Respostas:

220

Você pode conseguir o que deseja sem a instanceofpalavra - chave, já que pode escrever protetores de tipo personalizados agora:

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

Muitos membros

Se você precisar verificar vários membros para determinar se um objeto corresponde ao seu tipo, adicione um discriminador. O exemplo abaixo é o mais básico e exige que você gerencie seus próprios discriminadores ... você precisará se aprofundar nos padrões para garantir que você evite duplicados discriminadores.

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
fonte
85
"Não há como verificar a interface em tempo de execução." Eles ainda não o implementaram por qualquer motivo.
trusktr
16
E se a interface tiver 100 membros, você precisará verificar todos os 100? Foobar.
Jenny O'Reilly
4
Você pode adicionar um discriminador para o seu objeto, em vez de verificar todos os 100 ...
Fenton
7
esse paradigma discriminador (como está escrito aqui) não suporta interfaces estendidas. Uma interface derivada retornará false se verificar se é uma instanceOf de uma interface base.
Aaron
1
@ Fenton Talvez eu não saiba o suficiente sobre isso, mas suponha que você tenha uma interface B que estenda a interface A, que queira isInstanceOfA(instantiatedB)retornar true, mas que queira isInstanceOfB(instantiatedA)retornar false. Para que isso ocorra, o discriminador de B não precisa ser 'I-AM-A'?
Aaron
87

No TypeScript 1.6, a proteção de tipo definida pelo usuário fará o trabalho.

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, como Joe Yang mencionou: desde o TypeScript 2.0, você pode tirar vantagem do tipo de união com tags.

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 isso também funciona switch.

vilicvane
fonte
1
Isso parece bastante curioso. Aparentemente, existe algum tipo de meta-informação disponível. Por que expô-lo com esta sintaxe de proteção de tipo. Devido a quais restrições o "objeto é interface" ao lado de uma função funciona, em oposição à instância de? Mais precisamente, você poderia usar "object is interface" nas instruções if diretamente? Mas, em qualquer caso, sintaxe muito interessante, +1 de mim.
Lhk 25/12/15
1
@lhk Não existe essa declaração, é mais como um tipo especial que informa como um tipo deve ser restringido dentro de ramos condicionais. Devido ao "escopo" do TypeScript, acredito que não haverá tal declaração nem no futuro. Outra diferença entre object is typee object instanceof classé que, o TypeScript é estrutural, importa apenas a "forma", e não de onde um objeto obteve a forma: um objeto simples ou uma instância de uma classe, isso não importa.
vilicvane
2
Apenas para esclarecer um equívoco que essa resposta pode criar: não há meta informações para deduzir o tipo de objeto ou sua interface durante o tempo de execução.
mostruash 30/03
@mostruash Sim, a segunda metade da resposta não funcionará em tempo de execução, mesmo que seja compilada.
trusktr
4
Ah, mas isso deve assumir que, em tempo de execução, esses objetos foram criados com uma typepropriedade Nesse caso, funciona. Esse exemplo não mostra esse fato.
trusktr 16/02
40

typescript 2.0 introduzir união marcada

Recursos do 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
fonte
Estou usando a versão 2.0 beta, mas a união com tags não funciona. <TypeScriptToolsVersion> 2.0 </TypeScriptToolsVersion>
#
Compilado com construção noturna, mas o intellisense não funciona. Ele também lista os erros: Largura / tamanho da propriedade / ... não existe no Tipo 'Quadrado | Retângulo Círculo na declaração do caso. Mas compila.
makla
23
Isso é realmente apenas usando um discriminador.
Erik Philips
32

E quanto às proteções de tipo definidas pelo usuário? 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
fonte
3
Esta é minha resposta favorita - semelhante a stackoverflow.com/a/33733258/469777, mas sem seqüências de caracteres mágicas que podem quebrar devido a coisas como minificação.
Stafford Williams
1
Por algum motivo, isso não funcionou para mim, mas (pet as Fish).swim !== undefined;funcionou.
CyberMew 9/07/19
18

Agora é possível, acabei de lançar uma versão aprimorada do TypeScriptcompilador que fornece recursos completos de reflexão. Você pode instanciar classes de seus objetos de metadados, recuperar metadados de construtores de classe e inspecionar interface / classes em tempo de execução. Você pode conferir aqui

Exemplo de uso:

Em um dos seus arquivos datilografados, crie uma interface e uma classe que a implemente da seguinte maneira:

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

class MyClass implements MyInterface {
    counter = 0;

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

agora vamos imprimir algumas da lista de interfaces implementadas.

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

compile com o reflec-ts e inicie-o:

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

Veja reflection.d.ts para Interfacedetalhes do tipo meta.

UPDATE: Você pode encontrar um exemplo de trabalho completo aqui

vasculhar
fonte
8
downvoted porque eu pensei que isso era estúpido, mas depois parei por um segundo, olhei para a sua página do github e vi que ela era mantida atualizada e bem documentada e tão bem votada :-) Eu ainda não posso justificar usá-la agora mesmo apenas por implementsmas queria reconhecer o seu empenho e não queria ser mau :-)
Simon_Weaver
5
Na verdade, o principal objetivo que vejo desses recursos de reflexão é criar melhores estruturas de IoC, como as que o mundo Java já tem há muito tempo (a Primavera é a primeira e mais importante). Acredito firmemente que o TypeScript pode se tornar uma das melhores ferramentas de desenvolvimento do futuro e a reflexão é um dos recursos que realmente precisa.
pcan 24/08/16
5
... uh, e daí, temos que incluir esses "aprimoramentos" do compilador em qualquer versão futura do Typescript? Esta é efetivamente uma bifurcação do TypeScript, não do Typecript, certo? Nesse caso, essa não é uma solução viável a longo prazo.
Dudewad 07/12/16
1
@dudewad como dito em muitos outros tópicos, esta é uma solução temporária. Estamos aguardando a extensibilidade do compilador através de transformadores. Consulte os problemas relacionados no repositório oficial do TypeScript. Além disso, todas as linguagens de tipo forte amplamente adotadas têm reflexão, e acho que o TypeScript também deve ter. E, como eu, muitos outros usuários pensam assim.
usar o seguinte comando
Sim, não é que eu não concorde - eu também quero isso. Basta girar um compilador personalizado ... isso não significa que o próximo patch do Typescript precisa ser portado? Se você estiver mantendo isso, parabéns. Parece muito trabalho. Não batendo.
Dudewad
9

igual ao descrito acima, onde os protetores definidos pelo usuário foram usados, mas desta vez com um predicado de função de seta

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

Aqui está outra opção: o módulo ts-interface-builder fornece uma ferramenta em tempo de construção que converte uma interface TypeScript em um descritor de tempo de execução, e o ts-interface-checker pode verificar se um objeto a satisfaz.

Para o exemplo do OP,

interface A {
  member: string;
}

Você executaria primeiro o ts-interface-builderque produziria um novo arquivo conciso com um descritor, por exemplo foo-ti.ts, que você possa usar assim:

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" 

Você pode criar uma função de proteção de tipo de uma linha:

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

Gostaria de salientar que o TypeScript não fornece um mecanismo direto para testar dinamicamente se um objeto implementa uma interface específica.

Em vez disso, o código TypeScript pode usar a técnica JavaScript para verificar se um conjunto apropriado de membros está presente no objeto. Por exemplo:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}
Daniel Ribeiro
fonte
4
e se você tiver uma forma complexa? você não gostaria de codificar todas as propriedades em cada nível de profundidade #
Tom
@ Tom Acho que você pode passar (como um segundo parâmetro para a função verificador) um valor em tempo de execução ou exemplo / exemplo - ou seja, um objeto da interface que você deseja. Então, em vez de codificar código codificado, você escreve qualquer exemplo da interface que deseja ... e escreve um código único de comparação de objetos (usando, por exemplo for (element in obj) {}) para verificar se os dois objetos têm os elementos semelhantes de tipos semelhantes.
ChrisW
4

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
fonte
2
o "arg é MyInterfaced" é uma anotação interessante. O que acontece se isso falhar? Parece uma verificação da interface do tempo de compilação - que seria exatamente o que eu queria em primeiro lugar. Mas se o compilador verifica os parâmetros, por que ter um corpo de função? E se essa verificação é possível, por que movê-la para uma função separada.
Lhk
1
@lhk apenas ler typescript documentação sobre guardas tipo ... typescriptlang.org/docs/handbook/advanced-types.html
Dmitry Matveev
3

Com base na resposta de Fenton , aqui está minha implementação de uma função para verificar se um dado objecttem as chaves e um interfacetotal ou parcialmente.

Dependendo do seu caso de uso, também pode ser necessário verificar os tipos de cada uma das propriedades da interface. O código abaixo não faz isso.

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

Exemplo 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
fonte
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
fonte
1
Embora esse código possa responder à pergunta, fornecer um contexto adicional sobre por que e / ou como esse código responde à pergunta melhora seu valor a longo prazo.
Xiawi 18/10/19
0

Como o tipo é desconhecido no tempo de execução, escrevi o código da seguinte maneira para comparar o objeto desconhecido, não contra um tipo, mas contra um objeto do tipo conhecido:

  1. Crie um objeto de amostra do tipo certo
  2. Especifique quais de seus elementos são opcionais
  3. Faça uma comparação profunda do seu objeto desconhecido com esse objeto de amostra

Aqui está o código (independente de interface) que eu uso para uma comparação 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;
}

Abaixo está um exemplo de como eu o uso.

Neste exemplo, espero que o JSON contenha uma matriz de tuplas, da qual o segundo elemento é uma instância de uma interface chamada User(que possui dois elementos opcionais).

A verificação de tipo do TypeScript garantirá que meu objeto de amostra esteja correto, e a função assertTypeT verifica se o objeto desconhecido (carregado de JSON) corresponde ao objeto de amostra.

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

Você pode chamar uma verificação como esta na implementação de uma proteção de tipo definida pelo usuário.

ChrisW
fonte
0

Você pode validar um tipo TypeScript em tempo de execução usando ts-validate-type , assim: (embora exija um plug-in Babel):

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