Проверка типа интерфейса с помощью Typescript

294

Этот вопрос является прямым аналогом проверки типа класса с помощью TypeScript

Мне нужно выяснить во время выполнения, если переменная типа any реализует интерфейс. Вот мой код:

interface A{
    member:string;
}

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

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

Если вы введете этот код на игровой площадке, последняя строка будет помечена как ошибка: «Имя A не существует в текущей области». Но это не так, имя существует в текущей области видимости. Я даже могу изменить объявление переменной var a:A={member:"foobar"};без жалоб от редактора. После просмотра веб-страниц и поиска другого вопроса по SO я изменил интерфейс на класс, но затем я не могу использовать литералы объектов для создания экземпляров.

Я задавался вопросом, как тип A мог исчезнуть таким образом, но взгляд на сгенерированный javascript объясняет проблему:

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

Нет представления A как интерфейса, поэтому проверки типа во время выполнения невозможны.

Я понимаю, что javascript как динамический язык не имеет понятия интерфейсов. Есть ли способ проверки типа для интерфейсов?

Автозаполнение игровой площадки показывает, что машинопись даже предлагает метод implements. Как я могу использовать это?

ЛХК
источник
4
У JavaScript нет концепции интерфейсов, но это не потому, что это динамический язык. Это потому, что интерфейсы еще не реализованы.
trusktr
Да, но вы можете использовать класс вместо интерфейса. Смотрите этот пример.
Алексей Бараношников,
Видимо, не в 2017 году. Супер актуальный вопрос сейчас.
дабл-джош

Ответы:

221

Вы можете достичь того, чего хотите, без instanceofключевого слова, так как теперь вы можете создавать собственные охранники типов:

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

Много членов

Если вам нужно проверить множество членов, чтобы определить, соответствует ли объект вашему типу, вы можете вместо этого добавить дискриминатор. Ниже приведен самый базовый пример, который требует от вас управления вашими собственными дискриминаторами ... вам нужно углубиться в паттерны, чтобы избежать дублирования дискриминаторов.

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
«Нет способа проверить интерфейс во время выполнения». Есть, они просто еще не реализовали это по любой причине.
trusktr
16
А если в интерфейсе 100 участников, нужно проверить все 100? Foobar.
Дженни О'Рейли,
4
Вы можете добавить дискриминатор к вашему объекту, а не проверять все 100 ...
Фентон,
8
эта парадигма дискриминатора (как написано здесь) не поддерживает расширение интерфейсов. Производный интерфейс возвращает false, если проверяет, является ли он экземпляром базового интерфейса.
Аарон
1
@Fenton Возможно, я не знаю достаточно об этом, но предположим, что у вас был интерфейс B, который расширяет интерфейс A, вы хотите isInstanceOfA(instantiatedB)вернуть true, но вы хотите isInstanceOfB(instantiatedA)вернуть false. Для того, чтобы последнее произошло, не должен ли дискриминатор B быть «Я-Я-А»?
Аарон
88

В TypeScript 1.6 пользовательский тип guard будет выполнять эту работу.

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

И точно так же, как упоминал Джо Янг: начиная с TypeScript 2.0, вы даже можете воспользоваться преимуществами тегового объединенного типа.

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

И это switchтоже работает .

vilicvane
источник
1
Это выглядит довольно любопытно. Видимо, есть какая-то мета-информация. Зачем выставлять его с этим синтаксисом? Из-за каких ограничений «объект является интерфейсом» рядом с работой функции, в отличие от isinanceof? Точнее, вы могли бы напрямую использовать «объект - интерфейс» в операторах if? Но в любом случае очень интересный синтаксис, +1 от меня.
2015 года
1
@lhk Нет, такого утверждения нет, это больше похоже на специальный тип, который говорит, как следует сужать тип в условных ветвях. Из-за «охвата» TypeScript, я полагаю, такого заявления не будет даже в будущем. Другое различие между object is typeи object instanceof classзаключается в том, что тип в TypeScript является структурным, он заботится только о «форме», а не о том, откуда объект получил форму: обычный объект или экземпляр класса, это не имеет значения.
vilicvane
2
Этот ответ может создать просто неправильное представление: нет метаинформации для вывода типа объекта или его интерфейса во время выполнения.
Mostruash
@mostruash Да, вторая половина ответа не будет работать во время выполнения, даже если она компилируется.
trusktr
4
О, но это должно предполагать, что во время выполнения эти объекты будут созданы со typeсвойством. В этом случае это работает. Этот пример не показывает этот факт.
trusktr
40

машинопись 2.0 ввести теговое объединение

Особенности 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;
    }
}
Джо Ян
источник
Я использую 2.0 бета, но теговое соединение не работает. <TypeScriptToolsVersion> 2.0 </ TypeScriptToolsVersion>
Макла,
Скомпилировано с ночной сборкой, но intellisense не работает. В нем также перечислены ошибки: Свойство width / size / ... не существует в Type 'Square | Прямоугольник | Обведите кружок в выписке. Но это компилируется.
Макла
24
Это действительно просто использование дискриминатора.
Эрик Филипс
33

Как насчет пользовательских типов охранников? 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
Это мой любимый ответ - похожий на stackoverflow.com/a/33733258/469777, но без волшебных строк, которые могут сломаться из-за таких вещей, как минификация.
Стаффорд Уильямс
1
Это почему-то не сработало для меня, но сработало (pet as Fish).swim !== undefined;.
CyberMew
18

Теперь это возможно, я только что выпустил улучшенную версию TypeScriptкомпилятора, которая предоставляет возможности полного отражения. Вы можете создавать экземпляры классов из их объектов метаданных, извлекать метаданные из конструкторов классов и проверять интерфейс / классы во время выполнения. Вы можете проверить это здесь

Пример использования:

В одном из ваших файлов машинописи создайте интерфейс и класс, который реализует его следующим образом:

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

class MyClass implements MyInterface {
    counter = 0;

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

Теперь давайте распечатаем список реализованных интерфейсов.

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

скомпилируйте с отражением и запустите его:

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

См. Mirror.d.ts для Interfaceдеталей мета-типа.

ОБНОВЛЕНИЕ: Вы можете найти полный рабочий пример здесь

PCAN
источник
8
Я подумал, что это глупо, но потом сделал паузу на секунду, посмотрел на вашу страницу github и увидел, что она обновляется и хорошо документирована, поэтому вместо этого я проголосовал :-) Я до сих пор не могу оправдать использование этого сейчас прямо для implementsно хотел признать ваше обязательство и не хотел быть
злым
5
На самом деле, основная цель, которую я вижу в этих функциях отражения, - это создание более качественных IoC-фреймворков, подобных тем, которые мир Java уже имеет долгое время (Spring - первая и самая важная). Я твердо верю, что TypeScript может стать одним из лучших инструментов разработки будущего, а отражение - одна из функций, которая ему действительно нужна.
pcan
5
... ну и что, мы должны свернуть эти "улучшения" компилятора в любую будущую сборку Typescript? По сути, это форк Typescript, а не сам Typescript, верно? Если так, это не выполнимое долгосрочное решение.
dudewad
1
@dudewad, как говорилось во многих других темах, это временное решение. Мы ждем расширения компилятора через трансформеры. Пожалуйста, смотрите связанные проблемы в официальном репозитории TypeScript. Кроме того, все широко распространенные языки со строгим шрифтом имеют отражение, и я думаю, что в TypeScript это тоже должно быть. И, как и я, многие другие пользователи так думают.
pcan
да, я не согласен - я тоже этого хочу. Просто, раскручивая собственный компилятор ... разве это не означает, что следующий патч Typescript должен быть портирован? Если вы поддерживаете это, то слава. Просто похоже на большую работу. Не стучит.
dudewad
8

Вот еще один вариант: модуль ts-interface-builder предоставляет инструмент времени сборки, который преобразует интерфейс TypeScript в дескриптор времени выполнения, а ts-interface-checker может проверить, удовлетворяет ли объект этому.

Для примера OP,

interface A {
  member: string;
}

Сначала вы запустите, ts-interface-builderкоторый создаст новый краткий файл с дескриптором, скажем foo-ti.ts, который вы можете использовать так:

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" 

Вы можете создать однострочную функцию защиты типа:

function isA(value: any): value is A { return A.test(value); }
DS.
источник
6

Я хотел бы отметить, что TypeScript не предоставляет прямого механизма для динамического тестирования того, реализует ли объект конкретный интерфейс.

Вместо этого код TypeScript может использовать технику JavaScript для проверки наличия соответствующего объекта членов на объекте. Например:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}
Даниэль Рибейро
источник
4
Что делать, если у вас сложная форма? Вы не хотели бы жестко закодировать каждое свойство на каждом уровне глубины
Том
@ Том, я думаю, вы можете передать (как второй параметр функции проверки) значение времени выполнения или example / instancelar - то есть объект интерфейса, который вы хотите. Затем, вместо жесткого кода, вы пишете любой пример интерфейса, который вам нужен ... и пишете какой-то одноразовый код сравнения объектов (используя, например for (element in obj) {}), чтобы убедиться, что два объекта имеют одинаковые элементы схожих типов.
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» - интересная аннотация. Что произойдет, если это не удастся? Похоже, проверка интерфейса во время компиляции - это было бы именно то, что я хотел в первую очередь. Но если компилятор проверяет параметры, зачем вообще иметь тело функции. И если такая проверка возможна, зачем перемещать ее в отдельную функцию.
ЛХК
1
@lhk только что прочитал документацию по машинописи о стражах типов ... typescriptlang.org/docs/handbook/advanced-types.html
Дмитрий Матвеев
3

Основываясь на ответе Фентона , вот моя реализация функции, чтобы проверить, есть ли у данного objectключи interface, как полностью, так и частично.

В зависимости от вашего варианта использования вам также может понадобиться проверить типы каждого из свойств интерфейса. Код ниже не делает этого.

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

Пример использования:

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
источник
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
Хотя этот код может ответить на вопрос, предоставление дополнительного контекста относительно того, почему и / или как этот код отвечает на вопрос, повышает его долгосрочную ценность.
xiawi
0

Поскольку тип неизвестен во время выполнения, я написал следующий код для сравнения неизвестного объекта не с типом, а с объектом известного типа:

  1. Создайте образец объекта правильного типа
  2. Укажите, какие из его элементов являются необязательными
  3. Сделайте глубокое сравнение вашего неизвестного объекта с этим образцом объекта

Вот код (независимый от интерфейса), который я использую для глубокого сравнения:

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

Ниже приведен пример того, как я его использую.

В этом примере я ожидаю, что JSON содержит массив кортежей, вторым элементом которого является экземпляр интерфейса User(который имеет два необязательных элемента).

Проверка типов в TypeScript обеспечит правильность моего образца объекта, а затем функция assertTypeT проверяет, что неизвестный (загруженный из JSON) объект соответствует объекту образца.

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

Вы можете вызвать такую ​​проверку в реализации определяемой пользователем защиты типа.

ChrisW
источник
0

Вы можете проверить тип TypeScript во время выполнения, используя ts-validate-type , например так (хотя для этого требуется плагин Babel):

const user = validateType<{ name: string }>(data);
edbentley
источник