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