javascript - Verificarea tipului interfeței cu Typescript
Această întrebare este analogul direct al verificării tipului Class în TypeScript
Trebuie să aflu în timpul rulării dacă o variabilă de tip any implementează o interfață. Iată codul meu:
interface A {
member: string;
}
var a: any = { member: "foobar" };
if (a instanceof A) alert(a.member);
Dacă introduci acest cod în playground-ul TypeScript, ultima linie va fi marcată ca eroare, „Numele A nu există în domeniul curent”. Dar acest lucru nu este adevărat, numele există în domeniul curent. Pot chiar să schimb declarația variabilei în var a:A={member:"foobar"}; fără obiecții din partea editorului. După ce am navigat pe web și am găsit cealaltă întrebare de pe SO, am schimbat interfața într-o clasă, dar atunci nu mai pot folosi literale de obiect pentru a crea instanțe.
Mă întrebam cum ar fi putut tipul A să dispară astfel, dar o privire asupra javascript-ului generat explică problema:
var a = {
member: "foobar"
};
if (a instanceof A) {
alert(a.member);
}
Nu există nicio reprezentare a lui A ca interfață, prin urmare nu sunt posibile verificări de tip la rulare.
Înțeleg că JavaScript, ca limbaj dinamic, nu are conceptul de interfețe. Există vreo modalitate de a verifica tipurile pentru interfețe?
Autocompletarea din playground-ul TypeScript dezvăluie că TypeScript oferă chiar și o metodă implements. Cum o pot folosi?
Răspuns #1
Poți realiza ceea ce dorești fără cuvântul cheie instanceof, deoarece acum poți scrie garduri de tip personalizate:
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);
}
Mulți membri
Dacă trebuie să verificați mulți membri pentru a determina dacă un obiect corespunde tipului dvs., ați putea în schimb să adăugați un discriminator. Mai jos este cel mai de bază exemplu și necesită să vă gestionați propriile discriminatoare... va trebui să aprofundați în tipare pentru a vă asigura că evitați discriminatorii duplici.
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);
}
Răspunsul #2
În TypeScript 1.6, păzitorul de tip definit de utilizator va face treaba.
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 exact așa cum a menționat Joe Yang: din TypeScript 2.0, poți chiar să profiți de tipul union etichetat.
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 funcționează și cu switch.
Răspunsul #3
Ce zici de Gardurile de Tip Definit de Utilizator? 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();
}
Răspuns #4
typescript 2.0 introduce uniuni etichetate
Funcționalități 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;
}
}
Răspunsul #5
Acum este posibil, tocmai am lansat o versiune îmbunătățită a compilatorului TypeScript care oferă capacități complete de reflecție. Poți instanția clase din obiectele lor de metadata, poți obține metadata din constructorii claselor și poți inspecta interfețele/clasele la timpul de execuție. Poți verifica aici
Exemplu de utilizare:
În unul dintre fișierele tale TypeScript, creează o interfață și o clasă care să o implementeze astfel:
interface MyInterface {
doSomething(what: string): number;
}
class MyClass implements MyInterface {
counter = 0;
doSomething(what: string): number {
console.log('Doing ' + what);
return this.counter++;
}
}
Acum să tipărim o parte din lista interfețelor implementate.
for (let classInterface of MyClass.getClass().implements) {
console.log('Implemented interface: ' + classInterface.name)
}
compilare cu reflec-ts și lansarea acestuia:
node main.js
Implemented interface: MyInterface
Member name: counter - member kind: number
Member name: doSomething - member kind: function
Consultați reflection.d.ts pentru detalii despre meta-tipul Interface.
ACTUALIZARE: Puteți găsi un exemplu complet funcțional aici
Răspunsul #6
Iată o altă opțiune: modulul ts-interface-builder oferă un instrument la timpul de construire care convertește o interfață TypeScript într-un descriptor la timpul de execuție, și ts-interface-checker poate verifica dacă un obiect o satisface.
Pentru exemplul OP-ului,
interface A {
member: string;
}
Mai întâi ai rula ts-interface-builder, care produce un fișier nou concis cu un descriptor, să zicem, foo-ti.ts, pe care îl poți folosi astfel:
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"
Poți crea o funcție de protecție a tipului într-o singură linie:
function isA(value: any): value is A { return A.test(value); }
Răspunsul #7
Gărzi de tip în Typescript:
TS are garduri de tip în acest scop. Le definesc în următorul mod:
O expresie care efectuează o verificare la runtime care garantează tipul într-un anumit domeniu.
Aceasta înseamnă practic că compilatorul TS poate restrânge tipul la un tip mai specific atunci când are suficiente informații. De exemplu:
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
}
}
Revenind la întrebarea ta, putem aplica acest concept de garduri de tip și la obiecte pentru a determina tipul lor. Pentru a defini un gard de tip pentru obiecte, trebuie să definim o funcție al cărei tip de returnare este un predicat de tip. De exemplu:
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();
}
Răspunsul #8
Se apropie de 9 ani de la OP, iar această problemă persistă. Îmi doresc cu adevărat, CU ADEVĂRAT, să iubesc Typescript. Și de obicei reușesc. Dar breșele sale în siguranța tipurilor emană un miros urât pe care nasul meu strâmt nu-l poate bloca.
Solutiile mele de bază nu sunt perfecte. Dar opinia mea este că ele sunt mai bune decât majoritatea soluțiilor prescrise mai frecvent. Discriminatorii s-au dovedit a fi o practică proastă pentru că limitează scalabilitatea și anulează complet scopul siguranței tipurilor. Cele mai două soluții ale mele, urâte dar frumoase, sunt, în ordine:
Decorator de clasă: Scanează recursiv membrii obiectului tipizat și calculează un hash bazat pe numele simbolurilor. Asociază hash-ul cu numele tipului într-o proprietate KVP statică. Include numele tipului în calculul hash-ului pentru a diminua riscul de ambiguitate cu strămoșii (se întâmplă la subclase goale). Avantaje: S-a dovedit a fi cel mai de încredere. De asemenea, oferă implementări foarte stricte. Aceasta este de asemenea similară cu modul în care alte limbaje de nivel înalt implementează în mod nativ polimorfismul. Totuși, soluția necesită o extindere mult mai mare pentru a fi cu adevărat polimorfă. Dezavantaje: Obiectele anonime/JSON trebuie rehash-uite la fiecare verificare de tip, deoarece nu au definiții de tip cu care să se asocieze și să fie stocate în cache static. Suprasarcina excesivă a stivei duce la blocaje semnificative de performanță în scenarii de încărcare mare. Poate fi mitigată cu containere IoC, dar acest lucru poate fi, de asemenea, o suprasarcină nedorită pentru aplicațiile mici fără altă justificare. Necesită, de asemenea, o atenție suplimentară pentru a aplica decoratorul fiecărui obiect care necesită acest lucru.
Clonarea: Foarte urâtă, dar poate fi benefică cu strategii bine gândite. Creează o nouă instanță a obiectului tipizat și copiază reflexiv atribuțiile membrilor de nivel superior din obiectul anonim. Având un standard prestabilit pentru trecere, poți verifica și clona-casta simultan către tipuri. Ceva asemănător cu „tryParse” din alte limbi. Avantaje: În anumite scenarii, suprasarcina de resurse poate fi redusă prin utilizarea imediată a instanței „test” convertite. Nu este necesară diligență suplimentară pentru decoratori. O cantitate mare de toleranțe privind flexibilitatea. Contra: Scurgeri de memorie ca un sită de făină. Fără un clonare „profundă”, referințele modificate pot afecta alte componente care nu se așteaptă la încălcarea încapsulării. Memorarea în cache statică nu este aplicabilă, astfel că operațiile sunt executate la fiecare apel - obiectele cu un număr mare de membri la nivel superior vor afecta performanța. Dezvoltatorii noi în Typescript te vor confunda cu un junior din cauza neînțelegerii motivului pentru care ai scris acest tip de pattern.
În total: Nu cumpăr scuza „JS nu îl suportă” pentru nuanțele TypeScript în polimorfism. Transpilatoarele sunt absolut potrivite pentru acest scop. Ca să fie rănițele stropite cu sare: provine de la Microsoft. Ei au rezolvat aceeași problemă cu mulți ani în urmă cu mare succes: .Net Framework a oferit un API Interop robust pentru adoptarea compatibilității înapoi cu COM și ActiveX. Nu au încercat să transpilizeze pentru runtime-urile mai vechi. Acea soluție ar fi fost mult mai ușoară și mai puțin încâlcită pentru un limbaj lax și interpretat ca JS... totuși ei s-au speriat de teama de a nu pierde teren în fața altor superset-uri. Folosirea chiar a lipsurilor din JS, care ar fi trebuit rezolvate de TS, ca un fundament deformal pentru redefinirea principiului static tipizat al programării orientate pe obiecte este--ei bine--nonsense. Contrazice volumele de documentație și specificații de top din industrie care au informat dezvoltarea software de nivel înalt timp de decenii.
Răspunsul #9
În opinia mea, aceasta este cea mai bună abordare; atașați un simbol „Fubber” interfețelor. Este MULT mai rapid de scris, MULT mai rapid pentru motorul JavaScript decât un tip de protecție (type guard), suportă moștenirea pentru interfețe și face ca scrierea de tipuri de protecție să fie ușoară dacă aveți nevoie de ele.
Acesta este scopul pentru care ES6 are simboluri.
Interfață
// 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;
}
Clasă
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;
}
Testare
Acest cod poate fi pus într-un gard de tip dacă vrei să ajuți compilatorul TypeScript.
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
}
Răspuns #10
Aș dori să subliniez că TypeScript nu oferă un mecanism direct pentru a testa în mod dinamic dacă un obiect implementează o anumită interfață.
În schimb, codul TypeScript poate folosi tehnica JavaScript de a verifica dacă un set corespunzător de membri este prezent pe obiect. De exemplu:
var obj : any = new Foo();
if (obj.someInterfaceMethod) {
...
}
Răspuns #11
la fel ca mai sus, unde au fost folosite garduri definite de utilizator, dar de data aceasta cu un predicat funcție săgeată
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);
