typescript - Getter funkcija za vraćanje vrijednosti iz kolekcije s ispravnim tipom
Susreo sam se s naizgled nerješivim problemom u Typescriptu. Ako konstruiram generičku kolekciju čiji je oblik definiran izvan klase, Typescript ispravno razumije tipove kada se izravno pozivam na njezine vrijednosti, ali ne i kada koristim getter funkciju.
U ovom primjeru želim kolekciju u kojoj programer mora definirati ključeve za ono što se u njoj nalazi, ali joj može dati gotovo bilo koji željeni oblik.
Prvo, definirao sam generičku klasu za unose u kolekciji:
class Entry<T extends string | number> {
value?: T;
constructor(value: T) {
this.value = value;
}
}
Sljedeće, definirao sam klasu za samu kolekciju:
export type CollectionState<
Keys extends { [key: string]: string },
> = {
[key in Keys[keyof Keys]]: Entry<string> | Entry<number>;
};
class Collection<
Keys extends { [key: string]: string },
State extends CollectionState<Keys>,
> {
state: State;
constructor(initialState: State) {
this.state = initialState;
}
}
I ovdje sam stvorio kolekciju za testiranje:
enum MyKeys {
KEY1 = 'key1',
KEY2 = 'key2',
}
type MyKeysType = typeof MyKeys;
// Define a shape for the collection
interface StateDefinition {
[MyKeys.KEY1]: Entry<string>,
[MyKeys.KEY2]: Entry<number>,
}
// Define an initial state for the collection
const initialState: StateDefinition = {
[MyKeys.KEY1]: new Entry<string>('key1'),
[MyKeys.KEY2]: new Entry<number>(2),
};
// Create a new instance of the collection
const collection = new Collection<
MyKeysType,
StateDefinition
>(initialState);
Do sada, sve se slaže. Kada pozivam varijable unutar kolekcije i pregledavam ih, vidim da je key1 tipa Entry<string> i da je key2 tipa Entry<number>.
// All checks out
const test1 = collection.state[MyKeys.KEY1]; // type is Entry<string>
const test2 = collection.state.key1; // type is Entry<string>
const test3 = collection.state[MyKeys.KEY2]; // type is Entry<number>
const test4 = collection.state.key2; // type is Entry<number>
Problem se pojavljuje kada pokušam uključiti funkciju za dobivanje podataka. U ovom primjeru sam proširio novu klasu, ali se problem pojavljuje i s proširivanjem i bez njega.
class ExtendedCollection<
Keys extends { [key: string]: string },
State extends CollectionState<Keys>,
> extends Collection<Keys, State> {
getEntry(key: Keys[keyof Keys]) {
return this.state[key];
}
}
Očekivao sam da će Typescript ispravno zaključiti tipove prilikom pozivanja ove funkcije, ali nije tako.
const collection2 = new ExtendedCollection<
MyKeysType,
StateDefinition
>(initialState);
// All good
const test5 = collection2.state[MyKeys.KEY1]; // type is Entry<string>
const test6 = collection2.state.key1; // type is Entry<string>
const test7 = collection2.state[MyKeys.KEY2]; // type is Entry<number>
const test8 = collection2.state.key2; // type is Entry<number>
// FAIL!!!
const test9 = collection2.getEntry(MyKeys.KEY1); // type is Entry<number> | Entry<string>
const test10 = collection2.getEntry(MyKeys.KEY2); // type is Entry<number> | Entry<string>
Ne mogu ni odrediti točan tip koji bi trebao biti:
/*
TS2322: Type 'Entry<string> | Entry<number>' is not assignable to type 'Entry<string>'.
Type 'Entry<number>' is not assignable to type 'Entry<string>'.
Type 'number' is not assignable to type 'string'.
*/
const test11: Entry<string> = collection2.getEntry(MyKeys.KEY1);
/*
TS2322: Type 'Entry<string> | Entry<number>' is not assignable to type 'Entry<number>'.
Type 'Entry<string>' is not assignable to type 'Entry<number>'.
Type 'string' is not assignable to type 'number'.
*/
const test12: Entry<number> = collection2.getEntry(MyKeys.KEY2);
Posljednje što sam pokušao bilo je stvoriti nekoliko preopterećenja funkcije getEntry.
class ExtendedCollection2<
Keys extends { [key: string]: string },
State extends CollectionState<Keys>,
> extends Collection<Keys, State> {
getEntry(key: Keys[keyof Keys]): Entry<number>
getEntry(key: Keys[keyof Keys]): Entry<string>
getEntry(key: Keys[keyof Keys]): any {
return this.state[key];
}
}
Gradi se, ali još uvijek ne dobivam željeni efekt. Sada vidim da se obje vrijednosti vraćaju kao Entry<number> iako jedna od njih trebala biti Entry<string>.
const collection3 = new ExtendedCollection2<
MyKeysType,
StateDefinition
>(initialState);
// All good
const test13 = collection3.state[MyKeys.KEY1]; // type is Entry<string>
const test14 = collection3.state.key1; // type is Entry<string>
const test15 = collection3.state[MyKeys.KEY2]; // type is Entry<number>
const test16 = collection3.state.key2; // type is Entry<number>
// Does not infer the correct type based on the key provided
const test17 = collection3.getEntry(MyKeys.KEY1); // type is Entry<number>
const test18 = collection3.getEntry(MyKeys.KEY2); // type is Entry<number>
/*
TS2322: Type 'Entry<string> | Entry<number>' is not assignable to type 'Entry<string>'.
Type 'Entry<number>' is not assignable to type 'Entry<string>'.
Type 'number' is not assignable to type 'string'.
*/
const test19: Entry<string> = collection3.getEntry(MyKeys.KEY1);
// Okay
const test20: Entry<number> = collection3.getEntry(MyKeys.KEY2);
Kako mogu stvoriti funkciju koja može dohvatiti unos, ali ga ne vraća kao uniju svih mogućih tipova unutar zbirke?
Odgovor #1
TypeScript neće izvesti nikakve tipove, osim ako funkcija nije generička. Preopterećenja u vašem pitanju zapravo ne rade ništa, jer oba preopterećenja imaju iste tipove parametara. TypeScript će odabrati prvo koje odgovara argumentu, što dovodi do toga da je povratni tip uvijek Entry<number>.
Da bismo postigli željeno ponašanje, trebamo učiniti da tip key bude generički.
class ExtendedCollection<
Keys extends { [key: string]: string },
State extends CollectionState<Keys>,
> extends Collection<Keys, State> {
getEntry<K extends Keys[keyof Keys]>(key: K) {
return this.state[key];
}
}
Tip argumenta proslijeđenog funkciji getEntry koristit će se za izvođenje tipa K. Ne trebamo označavati povratni tip jer TypeScript ljubazno izvodi tip State[K] za nas.
const collection = new ExtendedCollection<MyKeysType, StateDefinition>(
initialState
);
const test1 = collection.getEntry(MyKeys.KEY1);
// ^? const test1: Entry<string>
const test2 = collection.getEntry(MyKeys.KEY2);
// ^? const test2: Entry<number>
Igralište
