Перевірка типу інтерфейсу за допомогою Typescript


294

Це питання є прямим аналогом перевірки типу класу за допомогою TypeScript

Мені потрібно з’ясувати під час виконання, чи змінна типу будь-який реалізує інтерфейс. Ось мій код:

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 році. Супер актуальне питання зараз.
doublejosh

Відповіді:


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? Фообар.
Jenny O'Reilly

4
Ви можете додати дискримінатора до свого об’єкта, а не перевірити всі 100 ...
Fenton

7
ця парадигма дискримінатора (як написано тут) не підтримує розширені інтерфейси. Похідний інтерфейс поверне помилкове значення, якщо перевірити, чи це екземпляр базового інтерфейсу.
Аарон

1
@Fenton Можливо, я недостатньо знаю про це, але припустимо, у вас був інтерфейс B, який розширює інтерфейс A, ви хочете isInstanceOfA(instantiatedB)повернути true, але ви хочете isInstanceOfB(instantiatedA)повернути false. Для останнього, чи не повинен дискримінатор B не бути «Я-AM-A»?
Аарон

87

У TypeScript 1.6 виконує завдання захищений тип, визначений користувачем .

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теж працює .


1
Це виглядає досить цікаво. Мабуть, є якась метаінформація. Навіщо виставляти його за допомогою цього синтаксису охоронного типу. У зв'язку з якими обмеженнями "об'єкт є інтерфейсом" поруч із функцією, на відміну від сутіфакту? Точніше, чи можна використовувати "object is interface" в операторах if, якщо безпосередньо? Але в будь-якому випадку, дуже цікавий синтаксис, +1 від мене.
lhk

1
@lhk Ні. Немає такого твердження, воно більше схоже на спеціальний тип, який говорить про те, як слід звузити тип усередині умовних гілок. Зважаючи на "сферу" роботи TypeScript, я вважаю, що такого твердження навіть у майбутньому не буде. Інша відмінність object is typeі object instanceof classполягає в тому, що тип в TypeScript є структурним, він дбає лише про "форму", а не де об'єкт отримав форму: звичайний об'єкт або екземпляр класу, це не має значення.
vilicvane

2
Просто для усунення помилкового уявлення ця відповідь може створити: немає мета-інформації для вирахування типу об'єкта чи його інтерфейсу під час виконання.
mostruash

@mostruash Так, друга половина відповіді не буде працювати під час виконання, навіть якщо вона складається.
trusktr

4
О, але це повинно припускати, що під час виконання ці об’єкти будуть створені з typeвластивістю. У цьому випадку це працює. Цей приклад не свідчить про цей факт.
trusktr

40

typecript 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>
Макла

Компілюється з нічним побудовою, але інтелігенція не працює. У ньому також перераховані помилки: Ширина / розмір властивості / ... не існує на Тип "Квадрат | Прямокутник | Коло в заяві справи Але вона компілює.
Макла

23
Це справді просто використання дискримінатора.
Ерік Філіпс

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

компілювати з reflec-t і запустити його:

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

Докладні відомості про Interfaceмета-тип див. У розділі refle.d.ts.

ОНОВЛЕННЯ: Повний робочий приклад ви можете знайти тут


8
тому що я подумав, що це дурно, але потім на секунду призупинився, переглянув вашу сторінку github і побачив, що вона оновлюється та добре задокументована, так замість цього :-) Я досі не можу виправдати, що використовую її зараз просто для implementsале хотів визнати вашу прихильність і не хотів бути середнім :-)
Simon_Weaver

5
Власне, основна мета, яку я бачу в цих відображеннях, - це створити кращі рамки IoC, як ті, які у світі Java вже давно (Весна - перша і найважливіша). Я твердо вірю, що TypeScript може стати одним з найкращих інструментів розвитку майбутнього, і рефлексія - одна з особливостей, яка йому справді потрібна.
pcan

5
... е-е, так що, ми маємо зафіксувати ці "удосконалення" компілятора в будь-якій майбутній збірці Typescript? Це фактично виделка Typescript, а не сам Typescript, правда? Якщо так, це не є можливим довгостроковим рішенням.
dudewad

1
@dudewad, як сказано в багатьох інших темах, це тимчасове рішення. Ми чекаємо розширення компілятора через трансформатори. Будь ласка, дивіться відповідні проблеми в офіційному репортажі TypeScript. Крім того, всі широко прийняті мови сильного типу мають відображення, і я думаю, що TypeScript повинен мати це також. І як я, багато інших користувачів так вважають.
pcan

так, це не те, що я не згоден - я теж хочу цього. Просто, запускаючи спеціальний компілятор ... хіба це не означає, що наступний патч Typescript потрібно перенести? Якщо ви дотримуєтесь цього, то кудо. Просто здається, що багато роботи. Не стукаючи.
dudewad


8

Ось ще один варіант: модуль ts-interface-builder забезпечує інструмент вбудованого часу, який перетворює інтерфейс TypeScript в дескриптор виконання та ts-interface-checker може перевірити, чи задовольняє його об'єкт.

На прикладі ОП:

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

6

Я хотів би зазначити, що TypeScript не забезпечує прямий механізм динамічного тестування, чи реалізує об'єкт певний інтерфейс.

Натомість код TypeScript може використовувати техніку JavaScript для перевірки наявності відповідного набору членів на об’єкті. Наприклад:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}

4
що робити, якщо у вас складна форма? ви б не хотіли жорстко кодувати кожну власність на кожному рівні глибини
Том,

@ Я думаю, ви можете передати (як другий параметр функції перевірки) значення часу виконання або приклад / sampleplar - тобто об'єкт інтерфейсу, який ви хочете. Потім замість жорсткого кодування ви пишете будь-який приклад інтерфейсу, який ви хочете ..., і пишете якийсь одноразовий код порівняння об'єктів (використовуючи наприклад 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
"аргумент MyInterfaced" - це цікава примітка. Що станеться, якщо це не вдасться? Схоже, перевірка інтерфейсу часу на компіляцію - це було б саме те, що я хотів в першу чергу. Але якщо компілятор перевіряє параметри, навіщо взагалі функціонувати тіло функції. І якщо така перевірка можлива, навіщо перенести її на окрему функцію.
lhk

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

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

Ви можете викликати такий чек у реалізації захищеного типу захисту.


0

Ви можете перевірити тип TypeScript під час виконання за допомогою ts-validate-типу , як-от так (для цього потрібен плагін Babel):

const user = validateType<{ name: string }>(data);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.