Jenis antarmuka periksa dengan naskah

292

Pertanyaan ini adalah analogon langsung ke cek tipe Kelas dengan TypeScript

Saya perlu mencari tahu pada saat runtime jika variabel jenis mengimplementasikan antarmuka. Ini kode saya:

interface A{
    member:string;
}

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

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

Jika Anda memasukkan kode ini di taman bermain naskah, baris terakhir akan ditandai sebagai kesalahan, "Nama A tidak ada dalam lingkup saat ini". Tapi itu tidak benar, namanya memang ada dalam ruang lingkup saat ini. Saya bahkan dapat mengubah deklarasi variabel menjadi var a:A={member:"foobar"};tanpa keluhan dari editor. Setelah menjelajahi web dan menemukan pertanyaan lain pada SO saya mengubah antarmuka ke kelas tapi kemudian saya tidak bisa menggunakan objek literal untuk membuat instance.

Saya bertanya-tanya bagaimana tipe A bisa menghilang seperti itu tetapi melihat javascript yang dihasilkan menjelaskan masalahnya:

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

Tidak ada representasi A sebagai antarmuka, oleh karena itu tidak ada pemeriksaan tipe runtime yang dimungkinkan.

Saya mengerti bahwa javascript sebagai bahasa dinamis tidak memiliki konsep antarmuka. Apakah ada cara untuk mengetik cek antarmuka?

Pelengkapan otomatis taman bermain naskah itu mengungkapkan bahwa naskah bahkan menawarkan metode implements. Bagaimana saya bisa menggunakannya?

lhk
sumber
4
JavaScript tidak memiliki konsep antarmuka, tetapi itu bukan karena itu adalah bahasa yang dinamis. Itu karena antarmuka belum diimplementasikan.
trusktr
Ya, tetapi Anda bisa menggunakan antarmuka kelas. Lihat contoh ini .
Alexey Baranoshnikov
Rupanya bukan pada tahun 2017. Pertanyaan super relevan sekarang.
doublejosh

Jawaban:

220

Anda dapat mencapai apa yang Anda inginkan tanpa instanceofkata kunci karena Anda dapat menulis penjaga tipe khusus sekarang:

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

Banyak Anggota

Jika Anda perlu memeriksa banyak anggota untuk menentukan apakah suatu objek cocok dengan tipe Anda, Anda bisa menambahkan pembeda. Di bawah ini adalah contoh paling dasar, dan mengharuskan Anda untuk mengelola diskriminator Anda sendiri ... Anda harus masuk lebih dalam ke pola untuk memastikan Anda menghindari duplikat diskriminator.

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
sumber
85
"Tidak ada cara untuk runtime memeriksa antarmuka." Ada, mereka hanya belum mengimplementasikannya untuk alasan apa pun.
trusktr
16
Dan jika antarmuka memiliki 100 anggota, Anda perlu memeriksa semua 100? Foobar.
Jenny O'Reilly
4
Anda dapat menambahkan pembeda ke objek Anda daripada memeriksa semua 100 ...
Fenton
7
paradigma diskriminator ini (seperti yang ditulis di sini) tidak mendukung perluasan antarmuka. Antarmuka yang diturunkan akan mengembalikan false jika memeriksa apakah itu adalah instance dari antarmuka dasar.
Aaron
1
@ Fenton Mungkin saya tidak cukup tahu tentang ini, tetapi anggap Anda memiliki antarmuka B yang memperluas antarmuka A, Anda ingin isInstanceOfA(instantiatedB)mengembalikan true, tetapi Anda ingin isInstanceOfB(instantiatedA)mengembalikan false. Agar yang terakhir terjadi, bukankah pembeda B tidak harus 'I-AM-A'?
Aaron
87

Dalam TypeScript 1.6, tipe penjaga yang ditentukan pengguna akan melakukan pekerjaan.

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

Dan seperti yang dikatakan Joe Yang: sejak TypeScript 2.0, Anda bahkan dapat mengambil keuntungan dari jenis tagged union.

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

Dan itu berhasil switchjuga.

vilicvane
sumber
1
Ini terlihat agak penasaran. Rupanya ada semacam meta-informasi yang tersedia. Mengapa memaparkannya dengan sintaks penjaga tipe ini. Karena kendala apa yang "objek adalah antarmuka" di sebelah fungsi berfungsi, yang bertentangan dengan isinstanceof? Lebih tepatnya, dapatkah Anda menggunakan "object is interface" di pernyataan if secara langsung? Namun bagaimanapun juga, sintaks yang sangat menarik, +1 dari saya.
lhk
1
@ lhk Tidak ada pernyataan seperti itu, itu lebih seperti tipe khusus yang memberitahu bagaimana seharusnya suatu jenis dipersempit di dalam cabang bersyarat. Karena "ruang lingkup" dari TypeScript, saya percaya tidak akan ada pernyataan seperti itu bahkan di masa depan. Perbedaan lain antara object is typedan object instanceof classadalah bahwa, mengetikkan TypeScript adalah struktural, ia hanya peduli pada "bentuk" dan bukannya dari mana objek mendapatkan bentuk: objek biasa atau turunan dari sebuah kelas, itu tidak masalah.
vilicvane
2
Hanya untuk menghapus kesalahpahaman, jawaban ini dapat dibuat: tidak ada informasi meta untuk mengurangi tipe objek atau antarmuka selama runtime.
mostruash
@mostruash Yap, bagian kedua dari jawaban tidak akan berfungsi saat runtime meskipun dikompilasi.
trusktr
4
Oh, tapi, ini harus mengasumsikan bahwa pada saat runtime objek ini akan dibuat dengan typeproperti. Dalam hal ini berfungsi. Contoh itu tidak menunjukkan fakta ini.
trusktr
40

typescript 2.0 memperkenalkan serikat yang ditandai

Fitur Script 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
sumber
Saya menggunakan 2.0 beta tetapi tagged union tidak berfungsi. <TypeScriptToolsVersion> 2.0 </TypeScriptToolsVersion>
Makla
Dikompilasi dengan bangunan malam, tetapi intellisense tidak berfungsi. Itu juga daftar kesalahan: Properti lebar / ukuran / ... tidak ada di Type 'Square | Kotak | Lingkari pernyataan kasus. Tapi itu mengkompilasi.
Makla
23
Ini benar-benar hanya menggunakan pembeda.
Erik Philips
32

Bagaimana dengan Pengawal Tipe Buatan Pengguna? 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
sumber
3
Ini adalah jawaban favorit saya - mirip dengan stackoverflow.com/a/33733258/469777 tetapi tanpa string ajaib yang dapat rusak karena hal-hal seperti minifikasi.
Stafford Williams
1
Ini tidak bekerja untuk saya untuk beberapa alasan tetapi (pet as Fish).swim !== undefined;berhasil.
CyberMew
18

Sekarang mungkin, saya baru saja merilis versi yang ditingkatkan dari TypeScriptkompiler yang menyediakan kemampuan refleksi penuh. Anda dapat membuat instance kelas dari objek metadata mereka, mengambil metadata dari konstruktor kelas dan memeriksa antarmuka / kelas saat runtime. Anda dapat memeriksanya di sini

Contoh penggunaan:

Di salah satu file naskah Anda, buat antarmuka dan kelas yang mengimplementasikannya seperti berikut:

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

class MyClass implements MyInterface {
    counter = 0;

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

sekarang mari kita cetak beberapa daftar antarmuka yang diimplementasikan.

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

kompilasi dengan reflec-ts dan luncurkan:

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

Lihat reflection.d.ts untuk Interfacedetail tipe-meta.

PEMBARUAN: Anda dapat menemukan contoh lengkap di sini

pcan
sumber
8
downvoted karena saya pikir ini bodoh, tapi kemudian berhenti sebentar, melihat halaman github Anda dan melihat itu terus up to date dan didokumentasikan dengan baik sehingga terbalik :-) Saya masih tidak dapat membenarkan menggunakannya sendiri sekarang hanya untuk implementstetapi ingin mengenali komitmen Anda dan tidak ingin menjadi kejam :-)
Simon_Weaver
5
Sebenarnya, tujuan utama yang saya lihat dari fitur refleksi ini adalah untuk menciptakan kerangka kerja IoC yang lebih baik seperti yang sudah dimiliki dunia Java sejak lama (Musim semi adalah yang pertama dan paling penting). Saya sangat percaya bahwa TypeScript dapat menjadi salah satu alat pengembangan terbaik di masa depan dan refleksi adalah salah satu fitur yang sangat dibutuhkan.
pcan
5
... eh, jadi apa, kita harus menggulung "penyempurnaan" kompiler ini ke dalam bentuk apa pun di masa depan dari Filescript? Ini secara efektif merupakan fork dari Typecrip, bukan Typecrip itu sendiri, kan? Jika demikian, ini bukan solusi jangka panjang yang layak.
dudewad
1
@dewaskan seperti yang dikatakan dalam banyak topik lain, ini adalah solusi sementara. Kami sedang menunggu ekstensi kompiler melalui transformer. Silakan lihat masalah terkait di repo TypeScript resmi. Selain itu, semua bahasa yang diketik kuat diadopsi memiliki refleksi, dan saya pikir TypeScript harus memilikinya juga. Dan seperti saya, banyak pengguna lain berpikir demikian.
pcan
ya bukan itu yang saya tidak setuju - saya ingin ini juga. Hanya, memutar kompiler khusus ... bukankah itu berarti patch selanjutnya dari ScriptScript perlu diporting? Jika Anda merawatnya maka pujian. Sepertinya banyak pekerjaan. Tidak mengetuknya.
dudewad
9

sama seperti di atas di mana penjaga yang ditentukan pengguna digunakan tetapi kali ini dengan predikat fungsi panah

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

Berikut opsi lain: modul ts-interface-builder menyediakan alat build-time yang mengubah antarmuka TypeScript menjadi deskriptor runtime, dan ts-interface-checker dapat memeriksa apakah suatu objek memuaskannya.

Sebagai contoh OP,

interface A {
  member: string;
}

Anda pertama kali menjalankan ts-interface-builderyang menghasilkan file ringkas baru dengan deskriptor, katakanlah foo-ti.ts, yang dapat Anda gunakan seperti ini:

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" 

Anda dapat membuat fungsi penjaga tipe satu-liner:

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

Saya ingin menunjukkan bahwa TypeScript tidak menyediakan mekanisme langsung untuk menguji secara dinamis apakah suatu objek mengimplementasikan antarmuka tertentu.

Sebaliknya, kode TypeScript dapat menggunakan teknik JavaScript untuk memeriksa apakah ada set anggota yang sesuai pada objek. Sebagai contoh:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}
Daniel Ribeiro
sumber
4
bagaimana jika Anda memiliki bentuk yang kompleks? Anda tidak ingin meng-hardcode setiap properti di setiap level kedalaman
Tom
@ Tom Saya kira Anda bisa memberikan (sebagai parameter kedua ke fungsi checker) nilai run-time atau contoh / contoh - yaitu objek dari antarmuka yang Anda inginkan. Kemudian, alih-alih kode hard-coding, Anda menulis contoh antarmuka apa pun yang Anda inginkan ... dan menulis beberapa kode perbandingan objek satu kali (menggunakan misal for (element in obj) {}) untuk memverifikasi bahwa dua objek memiliki elemen serupa dengan tipe yang sama.
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
sumber
2
"arg is MyInterfaced" adalah anotasi yang menarik. Apa yang terjadi jika itu gagal? Sepertinya pemeriksaan antarmuka waktu kompilasi - yang akan menjadi apa yang saya inginkan di tempat pertama. Tetapi jika kompiler memeriksa parameter, mengapa memiliki fungsi tubuh sama sekali. Dan jika pemeriksaan seperti itu dimungkinkan, mengapa memindahkannya ke fungsi terpisah.
lhk
1
@ lhk baru saja membaca dokumentasi naskah tentang pengawal tipe ... typescriptlang.org/docs/handbook/advanced-types.html
Dmitry Matveev
3

Berdasarkan jawaban Fenton , inilah penerapan fungsi saya untuk memverifikasi apakah yang diberikan objectmemiliki kunci interface, baik sepenuhnya atau sebagian.

Tergantung pada kasus penggunaan Anda, Anda mungkin juga perlu memeriksa jenis masing-masing properti antarmuka. Kode di bawah ini tidak melakukan itu.

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

Contoh penggunaan:

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
sumber
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
sumber
1
Sementara kode ini dapat menjawab pertanyaan, memberikan konteks tambahan tentang mengapa dan / atau bagaimana kode ini menjawab pertanyaan meningkatkan nilai jangka panjangnya.
xiawi
0

Karena tipe tidak diketahui pada saat run-time, saya menulis kode sebagai berikut untuk membandingkan objek yang tidak dikenal, bukan terhadap tipe, tetapi terhadap objek tipe yang dikenal:

  1. Buat objek sampel dengan tipe yang tepat
  2. Tentukan elemen mana yang opsional
  3. Lakukan perbandingan mendalam dari objek yang tidak dikenal dengan objek sampel ini

Inilah kode (antarmuka-agnostik) yang saya gunakan untuk perbandingan mendalam:

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

Di bawah ini adalah contoh bagaimana saya menggunakannya.

Dalam contoh ini saya berharap JSON berisi array tupel, di mana elemen kedua adalah instance dari antarmuka yang disebut User(yang memiliki dua elemen opsional).

Pengecekan typeScript akan memastikan bahwa objek sampel saya benar, maka fungsi assertTypeT memeriksa bahwa objek yang tidak diketahui (diambil dari JSON) cocok dengan objek sampel.

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

Anda bisa meminta cek seperti ini dalam implementasi tipe guard yang ditentukan pengguna.

ChrisW
sumber
0

Anda dapat memvalidasi tipe TypeScript saat runtime menggunakan tipe -ts-validate , seperti itu (memang membutuhkan plugin Babel):

const user = validateType<{ name: string }>(data);
edbentley
sumber