typescript - Konstruere et objekt ved å bruke en type av en generisk?


Jeg jobber med et offentlig RESTful API som returnerer objekter som har felter med URL-er som peker til mer informasjon. Jeg ønsker å pakke disse feltene inn i et objekt som angir hva de peker til, så jeg laget et objekt som dette:

class APIResource<T> {
    url: string;
}

Hvor T beskriver hva url skal returnere. T blir faktisk ikke engang brukt av objektet så langt Typescript er bekymret, men det hjelper meg å vite hva jeg bør forvente. Jeg jobber med Angular2, men vær oppmerksom på at det jeg prøver å gjøre ikke er Angular-spesifikt. Jeg har en API-klasse hvor jeg kan forespørre en URL og gi en type for å konvertere den til:

class Person {
  name: string;
  age: number;

  constructor(obj: any) {
    // all Constructable<T> classes will
    // have a constructor that looks
    // exactly like this
    Object.assign(this, obj);
  }

  print() {
    console.log(this.name + ' is ' + this.age);
  }
}

interface Constructable<T> {
  new (obj: any): T;
}

public get<T>(url: string, type: Constructable<T>): Observable<T> {
  return this.http.get(url).map(result => {
    return new type(result.json());
  });
}

I hovedsak lager jeg en ajax-forespørsel for å hente json fra API-et, jeg passerer det gjennom konstruktøren til typen min og returnerer objektet. Det ville bli brukt slik:

var person: Person;
api.get('/person/1/', Person).subscribe(p => person = p);

Dette fungerer hvis jeg har konstruktøren å sende inn, men det jeg gjerne vil gjøre er å skrive opp en getResource<T>(resource: APIResource<T>): Observable<T>-funksjon slik at jeg kunne gjøre noe slik som dette:

var res: APIResource<Person> = { url: '/person/1/' };
var person: Person;
api.getResource(res).subscribe(p => person = p);

Men så langt har ingenting fungert. Akkurat nå får jeg ikke typen T ut av APIResource for å bruke min nåværende metode. Jeg har prøvd å endre APIResource:

class APIResource<T> {
  url: string;

  getType(): T {
    return T;
  }
}

Eller

class APIResource<T extends Constructable<T>> {
  url: string;

  fetch(): T {
    // just a test to see if tsc complains
    // which it does with "cannot find name T"
    var obj: any = {};
    return new T(obj);
  }
}

Men ingen av dem fungerer. Jeg vet at typeinformasjonen forsvinner når det blir JavaScript. I stedet for at typen flytter seg rundt, tror jeg at jeg kunne klart å jobbe med å flytte konstruktøren rundt, men jeg tror ikke jeg kan gjøre det med generics.

Finnes det noen måte å få dette til å fungere med generics på?

Jeg bruker TypeScript 1.8.7

Svar #1

Her er en måte å gjøre det på i TypeScript med generics for å beholde sterk typethet:

interface Constructable<T> {
    ctor: new (...args) => T;
    url: string;
}

class APIResource<T> implements Constructable<T> {
    constructor(
        public ctor: new (...args) => T,
        public url: string
    ) {}
}

class Person {
    name: string;
    age: number;

    constructor(obj: any) {
        //Object.assign(this, obj); // requires ES6
        for (var key in obj) this[key] = obj[key];
    }

    print() {
        console.log(`${this.name} is ${this.age}`);
    }
}

function getResource<T>(apiResource: APIResource<T>): T {
    console.log(`resource url: "${apiResource.url}"`);

    // pretend this came back from an ajax request    
    let obj: any = { name: 'Arthur', age: 42 };
    console.log(`obj instanceof Person? ${obj instanceof Person}`); // false

    return new apiResource.ctor(obj);
}

const res = new APIResource(Person, '/person/1/');
let person = getResource(res);

console.log(`person instanceof Person? ${person instanceof Person}`); // true
person.print();

Jeg forenklet koden slik at den vil kjøre i en enkelt, selvstendig TypeScript-fil for å vise ideen uten å kreve Angular 2 og rxjs-avhengigheter. Å utvide den til en http-forespørsel i Angular 2 er imidlertid enkelt (delt inn i flere kart for klarhet):

getResource<T>(apiResource: APIResource<T>): Observable<T> {
    return this.http.get(apiResource.url)
        .map(result => result.json())
        .map(obj => new apiResource.ctor(obj));
}

Og deretter kan du konsumere det sterkt typede resultatet via subscribe, som vanlig.

Relaterte artikler