javascript - Provjera vrste sučelja s Typescriptom


Ovo pitanje je izravni pandan provjeri tipa klase u TypeScriptu

Trebam otkriti u runtime-u je li varijabla tipa any implementira sučelje. Evo mog koda:

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

Ako unesete ovaj kod u TypeScript playground, posljednja linija bit će označena kao pogreška, "Ime A ne postoji u trenutnom opsegu". Ali to nije istina, ime zapravo postoji u trenutnom opsegu. Mogu čak promijeniti deklaraciju varijable u var a:A={member:"foobar"}; bez prigovora od strane editora. Nakon pregledavanja weba i pronalaska drugog pitanja na SO, promijenio sam sučelje u klasu, ali tada ne mogu koristiti objektne literale za kreiranje instanci.

Pitao sam se kako bi tip A mogao nestati tako, ali pogled na generirani JavaScript objašnjava problem:

var a = {
    member: "foobar"
};

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

Ne postoji prikaz A kao sučelja, stoga nisu mogući provjere tipa u runtime-u.

Razumijem da javascript kao dinamički jezik nema koncept sučelja. Postoji li neki način da se provjeri tip za sučelja?

Autopopunjavanje u TypeScript playgroundu otkriva da TypeScript čak nudi metodu implements. Kako je mogu koristiti?

Odgovor #1

Možete postići ono što želite bez ključne riječi instanceof, jer sada možete napisati vlastite zaštite tipova:

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

Puno članova

Ako trebate provjeriti mnogo članova da biste utvrdili odgovara li objekt vašoj vrsti, umjesto toga možete dodati diskriminator. Dolje je najosnovniji primjer, i zahtijeva da sami upravljate svojim diskriminatorima... trebali biste se dublje upoznati s obrascima kako biste osigurali da izbjegnete duplicirane diskriminatore.

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

Odgovor #2

U TypeScriptu 1.6, korisnički definirana zaštita tipa obavit će posao.

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 kao što je Joe Yang spomenuo: od TypeScript 2.0, možete čak iskoristiti prednost tipa označenog unije.

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 funkcionira i s switch.

Odgovor #3

Što je s korisnički definiranim zaštitnicima tipova? 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();
}

Odgovor #4

TypeScript 2.0 uvodi označene unije

Značajke 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;
    }
}

Odgovor #5

Sada je moguće, upravo sam objavio poboljšanu verziju TypeScript kompajlera koja pruža potpune mogućnosti refleksije. Možete instancirati klase iz njihovih meta-podataka, dohvatiti meta-podatke iz konstruktora klasa i pregledavati sučelja/klase u runtime-u. Možete je provjeriti ovdje

Primjer uporabe:

U jednoj od vaših TypeScript datoteka, kreirajte sučelje i klasu koja ga implementira na sljedeći način:

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

class MyClass implements MyInterface {
    counter = 0;

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

sada ispišimo popis implementiranih sučelja.

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

kompilirajte s reflec-ts i pokrenite ga:

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

Pogledajte reflection.d.ts za detalje meta-tipa Interface.

AZURIRANJE: Puni funkcionalni primjer možete pronaći ovdje

Odgovor #6

Evo još jedne opcije: modul ts-interface-builder pruža alat za vrijeme izgradnje koji pretvara TypeScript sučelje u opisnik za vrijeme izvođenja, a ts-interface-checker može provjeriti zadovoljava li ga neki objekt.

Za OP-ov primjer,

interface A {
  member: string;
}

Prvo biste pokrenuli ts-interface-builder koji proizvodi novu sažetu datoteku s opisnikom, recimo, foo-ti.ts, koju možete koristiti ovako:

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žete stvoriti funkciju čuvara tipa u jednom retku:

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

Odgovor #7

Čuvari tipa u Typescriptu:

TS ima zaštitnike tipa za ovu svrhu. Oni to definiraju na sljedeći način:

Neki izraz koji izvodi provjeru u izvršnom vremenu koja jamči tip u nekom opsegu.

Ovo u osnovi znači da TS prevodilac može suziti tip na specifičniji tip kada ima dovoljno informacija. Na primjer:

function foo (arg: number | string) {
    if (typeof arg === 'number') {
        // fine, type number has toFixed method
        arg.toFixed()
    } else {
        // Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?
        arg.toFixed()
        // TSC can infer that the type is string because 
        // the possibility of type number is eliminated at the if statement
    }
}

Da se vratimo na vaše pitanje, ovaj koncept zaštitnika tipova možemo primijeniti i na objekte kako bismo odredili njihov tip. Da bismo definirali zaštitnika tipova za objekte, trebamo definirati funkciju čiji je povratni tip predikat tipa. Na primjer:

interface Dog {
    bark: () => void;
}

// The function isDog is a user defined type guard
// the return type: 'pet is Dog' is a type predicate, 
// it determines whether the object is a Dog
function isDog(pet: object): pet is Dog {
  return (pet as Dog).bark !== undefined;
}

const dog: any = {bark: () => {console.log('woof')}};

if (isDog(dog)) {
    // TS now knows that objects within this if statement are always type Dog
    // This is because the type guard isDog narrowed down the type to Dog
    dog.bark();
}

Odgovor #8

Približava se 9 godina od operacije, a ovaj problem i dalje postoji. Doista, DOISTA želim voljeti Typescript. I obično uspijem. Ali njegove rupe u sigurnosti tipova su odvratan miris koji moj stisnuti nos ne može blokirati.

Moja uobičajena rješenja nisu savršena. Ali moje mišljenje je da su bolja od većine češće propisanih rješenja. Diskriminatori su se pokazali lošom praksom jer ograničavaju skalabilnost i u potpunosti uništavaju svrhu sigurnosti tipova. Moja dva najljepša ružno-rješenja su, redom:

Dekorator klase: Rekurzivno pregledava članove tipiziranog objekta i izračunava hash na temelju imena simbola. Povezuje hash s imenom tipa u statičkoj KVP svojstvu. Uključi ime tipa u izračun hash-a kako bi se smanjio rizik od dvosmislenosti s pretcima (događa se kod praznih podklasa). Prednosti: Dokazano je da je najpouzdanije. Također pruža vrlo stroga ograničenja. Ovo je također slično načinu na koji drugi visoko razinski jezici izvorno implementiraju polimorfizam. Ipak, rješenje zahtijeva znatno dodatno proširenje kako bi zaista bilo polimorfno. Protiv: Anonimni/JSON objekti moraju se ponovno heširati pri svakoj provjeri tipa, budući da nemaju definicije tipa s kojima bi se mogli povezati i statički keširati. Pretjerani overhead stoga rezultira značajnim uskim grlima u izvedbi u scenarijima velikog opterećenja. To se može ublažiti s IoC kontejnerima, ali to također može biti neželjeni overhead za male aplikacije bez druge potrebe. Također zahtijeva dodatnu pažnju pri primjeni dekoratora na svaki objekt kojem je potreban.

Kloniranje: Vrlo ružno, ali može biti korisno uz promišljene strategije. Stvorite novu instancu tipiziranog objekta i refleksivno kopirajte dodjele članova na najvišoj razini iz anonimnog objekta. Uz unaprijed određeni standard za prolaz, možete istovremeno provjeriti i klonirati-u-tipu tipove. Nešto slično 'tryParse' iz drugih jezika. Prednosti: U određenim scenarijima, opterećenje resursa može se ublažiti trenutnom uporabom konvertirane 'test' instance. Nije potrebna dodatna pažnja za dekoratore. Velika količina tolerancije fleksibilnosti. Nedostaci: Curjenje memorije poput sita za brašno. Bez 'duboke' kloniranja, mutirane reference mogu pokvariti druge komponente koje ne očekuju narušavanje enkapsulacije. Statičko predmemoriranje nije primjenjivo, pa se operacije izvršavaju pri svakom pozivu—objekti s velikim brojem vrhunskih članova utjecat će na performanse. Programeri koji su novi u Typescriptu pogrešno će te smatrati juniorom jer neće razumjeti zašto si napisao ovu vrstu obrasca.

Sveukupno: Ne prihvaćam izliku 'JS to ne podržava' za nijanse Typescripta u polimorfizmu. Transpileri su apsolutno prikladni za tu svrhu. Da dodatno pogoršam: dolazi od Microsofta. Oni su ovaj isti problem riješili prije mnogo godina s velikim uspjehom: .Net Framework je ponudio robustan Interop API za usvajanje kompatibilnosti unatrag s COM-om i ActiveX-om. Nisu pokušavali transpileirati na starije runtimeove. To bi rješenje bilo mnogo lakše i manje kaotično za slobodan i interpretirani jezik poput JS... a ipak su se povukli iz straha od gubitka terena pred drugim supersetovima. Korištenje samih nedostataka u JS-u, koji su trebali biti riješeni TS-om, kao deformirane osnove za redefiniranje principa statički tipiziranog objektno-orijentiranog programiranja je—pa—besmisleno. To se kosi s golemim količinama industrijski vodeće dokumentacije i specifikacija koje su desetljećima informirale visokorazinu softverskog razvoja.

Odgovor #9

Po mom mišljenju, ovo je najbolji pristup; priložite simbol "Fubber" sučeljima. Pisanje je MNOGO brže, MNOGO je brže za JavaScript engine od type guarda, podržava nasljeđivanje sučelja i olakšava pisanje type guarda ako vam zatrebaju.

Ovo je svrha zbog koje ES6 ima simbole.

Sučelje

// Notice there is no naming conflict, because interfaces are a *type*
export const IAnimal = Symbol("IAnimal"); 
export interface IAnimal {
  [IAnimal]: boolean; // the fubber
}

export const IDog = Symbol("IDog");
export interface IDog extends IAnimal {
  [IDog]: boolean;
}

export const IHound = Symbol("IDog");
export interface IHound extends IDog {
  // The fubber can also be typed as only 'true'; meaning it can't be disabled.
  [IDog]: true;
  [IHound]: boolean;
}

Razred

import { IDog, IAnimal } from './interfaces';
class Dog implements IDog {
  // Multiple fubbers to handle inheritance:
  [IAnimal] = true;
  [IDog] = true;
}

class Hound extends Dog implements IHound {
  [IHound] = true;
}

Testiranje

Ovaj kod se može staviti u type guard ako želite pomoći TypeScript prevoditelju.

import { IDog, IAnimal } from './interfaces';

let dog = new Dog();

if (dog instanceof Hound || dog[IHound]) {
  // false
}
if (dog[IAnimal]?) {
  // true
}

let houndDog = new Hound();

if (houndDog[IDog]) {
  // true
}

if (dog[IDog]?) {
  // it definitely is a dog
}

Odgovor #10

Želio bih istaknuti da TypeScript ne pruža izravan mehanizam za dinamičko provjeravanje da li objekt implementira određeno sučelje.

Umjesto toga, TypeScript kod može koristiti JavaScript tehniku provjere prisutnosti odgovarajućeg skupa članova na objektu. Na primjer:

var obj : any = new Foo();

if (obj.someInterfaceMethod) {
    ...
}

Odgovor #11

isto kao gore gdje su korišteni čuvari definirani od strane korisnika, ali ovaj put s predikatom u obliku streličaste funkcije

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

Povezani članci