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.
