Sprawdzanie typu interfejsu za pomocą Typescript


294

To pytanie jest bezpośrednim sprawdzeniem analogonu do klasy za pomocą TypeScript

Muszę dowiedzieć się w czasie wykonywania, czy zmienna typu any implementuje interfejs. Oto mój kod:

interface A{
    member:string;
}

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

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

Jeśli wpiszesz ten kod na placu zabaw maszynopisu, ostatni wiersz zostanie oznaczony jako błąd: „Nazwa A nie istnieje w bieżącym zakresie”. Ale to nieprawda, nazwa istnieje w obecnym zakresie. Mogę nawet zmienić deklarację zmiennej na var a:A={member:"foobar"};bez reklamacji redaktora. Po przejrzeniu sieci i znalezieniu drugiego pytania na SO zmieniłem interfejs na klasę, ale potem nie mogę używać literałów obiektowych do tworzenia instancji.

Zastanawiałem się, jak typ A może tak zniknąć, ale spojrzenie na wygenerowany skrypt javascript wyjaśnia problem:

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

Nie ma reprezentacji A jako interfejsu, dlatego nie jest możliwe sprawdzenie typu środowiska wykonawczego.

Rozumiem, że javascript jako język dynamiczny nie ma pojęcia interfejsów. Czy jest jakiś sposób na sprawdzenie typu dla interfejsów?

Automatyczne uzupełnianie placu zabaw maszynopisu ujawnia, że ​​maszynopis oferuje nawet metodę implements. Jak mogę tego użyć?


4
JavaScript nie ma pojęcia interfejsów, ale nie dlatego, że jest to język dynamiczny. To dlatego, że interfejsy nie są jeszcze zaimplementowane.
trusktr

Tak, ale możesz użyć interfejsu klasy zamiast. Zobacz ten przykład.
Alexey Baranoshnikov,

Najwyraźniej nie w 2017 r. Pytanie bardzo istotne teraz.
doublejosh

Odpowiedzi:


221

Możesz osiągnąć to, co chcesz bez instanceofsłowa kluczowego, ponieważ możesz teraz pisać zabezpieczenia typu niestandardowego:

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

Wielu członków

Jeśli musisz sprawdzić wiele członków, aby ustalić, czy obiekt pasuje do twojego typu, możesz zamiast tego dodać dyskryminator. Poniższy przykład jest najbardziej podstawowym przykładem i wymaga zarządzania własnymi dyskryminatorami ... musisz pogłębić wzorce, aby uniknąć duplikowania dyskryminatorów.

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
„Nie ma sposobu, aby środowisko uruchomieniowe sprawdzało interfejs.” Po prostu nie wdrożyli go jeszcze z jakiegokolwiek powodu.
trusktr

16
A jeśli interfejs ma 100 członków, musisz sprawdzić wszystkie 100? Foobar.
Jenny O'Reilly

4
Możesz dodać dyskryminator do swojego obiektu, zamiast sprawdzać wszystkie 100 ...
Fenton

7
ten paradygmat dyskryminatora (jak napisano tutaj) nie obsługuje rozszerzania interfejsów. Interfejs pochodny zwróciłby wartość false, jeśli sprawdziłby, czy jest instancją interfejsu podstawowego.
Aaron

1
@Fenton Być może nie wiem o tym wystarczająco dużo, ale załóżmy, że masz interfejs B, który rozszerza interfejs A, chciałbyś isInstanceOfA(instantiatedB)zwrócić wartość true, ale chciałbyś isInstanceOfB(instantiatedA)zwrócić wartość false. Aby to drugie miało miejsce, czy dyskryminator B nie musiałby być „I-AM-A”?
Aaron

87

W TypeScript 1.6 strażnik typów zdefiniowany przez użytkownika wykona zadanie.

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

I tak jak wspomniał Joe Yang: od TypeScript 2.0 możesz nawet skorzystać z oznaczonego typu unii.

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

I to też działa switch.


1
To wygląda dość ciekawie. Najwyraźniej dostępne są jakieś meta-informacje. Po co ujawniać to za pomocą tej składni ochrony typu. Z powodu jakich ograniczeń „obiekt jest interfejsem” obok funkcji działa, w przeciwieństwie do isinstanceof? Mówiąc ściślej, czy można użyć „obiekt jest interfejsem” w instrukcjach if bezpośrednio? Ale w każdym razie bardzo interesująca składnia +1 ode mnie.
lhk,

1
@lhk Nie, nie ma takiego stwierdzenia, jest bardziej jak specjalny typ, który mówi, jak należy zawęzić typ w gałęziach warunkowych. Ze względu na „zakres” TypeScript uważam, że takiego oświadczenia nie będzie w przyszłości. Inna różnica między object is typei object instanceof classpolega na tym, że typ w TypeScript jest strukturalny, obchodzi go tylko „kształt” zamiast tego, skąd obiekt uzyskał kształt: zwykły obiekt lub instancja klasy, to nie ma znaczenia.
vilicvane

2
Aby usunąć nieporozumienie, ta odpowiedź może stworzyć: nie ma meta informacji, które mogłyby odjąć typ obiektu lub jego interfejs podczas działania.
mostruash

@mostruash Tak, druga połowa odpowiedzi nie działa w czasie wykonywania, nawet jeśli się kompiluje.
trusktr

4
Och, ale należy założyć, że w czasie wykonywania te obiekty zostaną utworzone za pomocą typewłaściwości. W takim przypadku to działa. Ten przykład nie pokazuje tego faktu.
trusktr

40

maszynopis 2.0 wprowadza oznaczony związek

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

Korzystam z wersji beta 2.0, ale oznaczony związek nie działa. <TypeScriptToolsVersion> 2.0 </TypeScriptToolsVersion>
Makla,

Kompilowany z kompilacją nocną, ale intellisense nie działa. Zawiera także listę błędów: szerokość / rozmiar / właściwość nie istnieje w polu Type 'Square | Prostokąt | Oświadczenie w sprawie w kręgu. Ale się kompiluje.
Makla,

23
Tak naprawdę to tylko użycie dyskryminatora.
Erik Philips

33

Co powiesz na zdefiniowane przez użytkownika zabezpieczenia typu? 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
To moja ulubiona odpowiedź - podobna do stackoverflow.com/a/33733258/469777, ale bez magicznych łańcuchów, które mogłyby się zepsuć z powodu takich rzeczy jak minifikacja.
Stafford Williams,

1
Z jakiegoś powodu to nie działało, ale zadziałało (pet as Fish).swim !== undefined;.
CyberMew

18

Jest to teraz możliwe, właśnie wydałem ulepszoną wersję TypeScriptkompilatora, która zapewnia pełne możliwości odbicia. Możesz tworzyć instancje klas z ich obiektów metadanych, pobierać metadane z konstruktorów klas i sprawdzać interfejs / klasy w czasie wykonywania. Możesz to sprawdzić tutaj

Przykład użycia:

W jednym ze swoich plików maszynopisu utwórz interfejs i klasę, która implementuje go w następujący sposób:

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

class MyClass implements MyInterface {
    counter = 0;

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

teraz wydrukujmy listę zaimplementowanych interfejsów.

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

skompiluj z reflec-ts i uruchom:

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

Szczegółowe informacje na temat Interfacemeta-typów znajdują się na stronie reflection.d.ts .

AKTUALIZACJA: Pełny przykład działa tutaj


8
przegłosowałem, bo myślałem, że to głupie, ale potem zatrzymałem się na sekundę, spojrzałem na twoją stronę github i zobaczyłem, że jest aktualizowana i dobrze udokumentowana, więc głosowałem :-) Nadal nie mogę usprawiedliwić korzystania z niej teraz tylko dla implementsale chciałem
wyrazić

5
W rzeczywistości głównym celem tych funkcji refleksji jest tworzenie lepszych frameworków IoC, takich jak te, które świat Java ma już od dawna (wiosna jest pierwszą i najważniejszą). Mocno wierzę, że TypeScript może stać się jednym z najlepszych narzędzi programistycznych w przyszłości, a refleksja jest jedną z funkcji, których naprawdę potrzebuje.
pcan

5
... uh, więc co, musimy wprowadzić te „ulepszenia” kompilatora do dowolnej przyszłej wersji Typescript? To faktycznie rozwidlenie maszynopisu, a nie samego maszynopisu, prawda? Jeśli tak, nie jest to wykonalne rozwiązanie długoterminowe.
dudewad

1
@dudewad, jak powiedziano w wielu innych tematach, jest to rozwiązanie tymczasowe. Czekamy na rozszerzenie kompilatora za pomocą transformatorów. Zobacz podobne problemy w oficjalnym repozytorium TypeScript. Co więcej, wszystkie powszechnie przyjęte języki pisane silnie na maszynie mają odbicie, i myślę, że TypeScript też powinien je mieć. I podobnie jak ja wielu innych użytkowników tak uważa.
pcan

tak, to nie tak, że się nie zgadzam - ja też tego chcę. Rozkręcenie niestandardowego kompilatora ... czy to nie znaczy, że następna łata Typescript musi zostać przeniesiona? Jeśli utrzymujesz to, to uznanie. Wydaje się, że to dużo pracy. Nie pukać.
dudewad


8

Oto kolejna opcja: moduł ts-interface-builder zapewnia narzędzie do kompilacji, które konwertuje interfejs TypeScript na deskryptor środowiska wykonawczego, a ts-interface-checker może sprawdzić, czy obiekt go spełnia.

Na przykład OP

interface A {
  member: string;
}

Najpierw uruchomisz, ts-interface-builderktóry tworzy nowy zwięzły plik z deskryptorem, powiedzmy foo-ti.ts, którego możesz użyć w następujący sposób:

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" 

Możesz utworzyć jednokierunkową funkcję ochrony typu:

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

6

Chciałbym zauważyć, że TypeScript nie zapewnia bezpośredniego mechanizmu do dynamicznego testowania, czy obiekt implementuje określony interfejs.

Zamiast tego kod TypeScript może korzystać z techniki JavaScript do sprawdzania, czy w obiekcie znajduje się odpowiedni zestaw elementów. Na przykład:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}

4
co jeśli masz złożony kształt? nie chcesz kodować na stałe każdej właściwości na każdym poziomie głębokości
Tom

@ Tom Myślę, że możesz przekazać (jako drugi parametr do funkcji sprawdzającej) wartość czasu wykonywania lub przykład / przykład - tj. Obiekt interfejsu, który chcesz. Następnie zamiast kodu na sztywno piszesz dowolny przykład interfejsu, który chcesz ... i piszesz jednorazowy kod porównywania obiektów (używając np. for (element in obj) {}), Aby sprawdzić, czy dwa obiekty mają podobne elementy podobnych typów.
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” to ciekawa adnotacja. Co się stanie, jeśli to się nie powiedzie? Wygląda jak kontrola interfejsu kompilacji czasu - która byłaby dokładnie tym, czego chciałem. Ale jeśli kompilator sprawdza parametry, to po co w ogóle mieć treść funkcji. A jeśli taka kontrola jest możliwa, po co przenosić ją do oddzielnej funkcji.
lhk

1
@lhk właśnie przeczytałem dokumentację maszynopisu na temat strażników typów ... typescriptlang.org/docs/handbook/advanced-types.html
Dmitry Matveev

3

W oparciu o odpowiedź Fentona , oto moja implementacja funkcji sprawdzającej, czy dana objectma klucze i interfacema, zarówno w pełni, jak i częściowo.

W zależności od przypadku użycia może być konieczne sprawdzenie typów właściwości każdego interfejsu. Poniższy kod tego nie robi.

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

Przykład użycia:

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
Chociaż ten kod może odpowiedzieć na pytanie, zapewnienie dodatkowego kontekstu dotyczącego tego, dlaczego i / lub jak ten kod odpowiada na pytanie, poprawia jego długoterminową wartość.
xiawi

0

Ponieważ typ jest nieznany w czasie wykonywania, napisałem kod w następujący sposób, aby porównać nieznany obiekt, nie z typem, ale z obiektem znanego typu:

  1. Utwórz przykładowy obiekt odpowiedniego typu
  2. Określ, który z jego elementów jest opcjonalny
  3. Dokonaj dokładnego porównania swojego nieznanego obiektu z tym obiektem przykładowym

Oto kod (niezależny od interfejsu), którego używam do głębokiego porównania:

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

Poniżej znajduje się przykład tego, jak go używam.

W tym przykładzie oczekuję, że JSON zawiera tablicę krotek, z których drugi element jest instancją interfejsu o nazwie User(która ma dwa opcjonalne elementy).

Sprawdzanie typu TypeScript zapewni, że mój przykładowy obiekt jest poprawny, a następnie funkcja assertTypeT sprawdzi, czy nieznany (załadowany z JSON) obiekt pasuje do obiektu przykładowego.

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

Możesz wywołać taką kontrolę w implementacji funkcji ochrony zdefiniowanej przez użytkownika.


0

Możesz sprawdzić typ TypeScript w czasie wykonywania, używając ts-validate-type , podobnie jak to (wymaga wtyczki Babel):

const user = validateType<{ name: string }>(data);
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.