[TypeScript] Generic types worden niet juist afgeleid

Pagina: 1
Acties:

Onderwerpen

Vraag


Acties:
  • +1 Henk 'm!

  • TFBlunt
  • Registratie: November 2010
  • Laatst online: 08-08 10:27
Hoi allemaal,

Het leek me leuk om eens aan de slag te gaan met generics in TypeScript. Ik probeer een data store te maken in Redux-stijl. Deze moet minstens drie operaties ondersteunen: SET, UPDATE and DELETE. De parameters zijn voor elke operatie verschillend.

Daarnaast wil ik het makkelijk maken om mijn store uit te breiden door het operatietype ook te generalizeren.

Ik heb twee voorbeelden gemaakt. In het eerste voorbeeld is het operatietype nog niet generiek. Daar worden de types ook afgeleid zoals ik het verwacht, zodat jullie kunnen zien waar ik naar toe wil.

In mijn tweede voorbeeld is het operatietype generiek. Echter herkent TypeScript niet alle attributen van mijn operaties, maar enkel de attributen die ALLE operaties gemeen hebben (type en description).

Ik declareer de generieke types als volgt:
JavaScript:
1
class Store<CONTEXT extends StoreContext, ACTION extends Action<CONTEXT> = Action<CONTEXT>>

En mijn reducer ziet er als volgt uit:

Leidt operatietype goed af:
JavaScript:
1
reduce(state: CONTEXT, action: Action<CONTEXT>): CONTEXT { }

Leidt operatietype niet goed af:
JavaScript:
1
reduce(state: CONTEXT, action: ACTION): CONTEXT { }

Blijkbaar declareer ik mijn generieke types niet op de juiste manier. Hebben jullie wellicht een suggestie hoe het wel moet?

Alvast bedankt!

Voorbeelden:
Werkt: https://stackblitz.com/ed...t?file=example%2Fworks.ts
Werkt niet: https://stackblitz.com/ed...=example%2Fdoesnt_work.ts

Alle reacties


Acties:
  • 0 Henk 'm!

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 21-08 11:35

.oisyn

Moderator Devschuur®

Demotivational Speaker

First off, ik heb 0 ervaring met TypeScript, maar ik weet wel het een en ander van compilerbouw.

Ik zie trouwens nergens reduce() aangeroepen worden. Bedoel je niet dispatch()?

Het probleem hier lijkt me dat de interpreter niet weet waar hij { type: ActionType.DELETE, description: options.description, path: options.path } mee moet matchen. Hij kan dat niet inferren uit de constraint van dispatch want dat is een generiek type. Hij zal eerst moeten bepalen wat het type is, daarna kan hij kijken of het aan de constraint voldoet. Een soort van cast lijkt me hier dus op zijn plek.

.edit: ok, nu ik wat meer heb gelezen over interfaces en generics in TypeScript lijkt me wat ik hier zeg niet helemaal te kloppen ;)

[ Voor 9% gewijzigd door .oisyn op 15-03-2019 12:59 ]

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Acties:
  • 0 Henk 'm!

  • TFBlunt
  • Registratie: November 2010
  • Laatst online: 08-08 10:27
Thanks voor de input!

Ik heb voor de duidelijkheid (dat was althans de bedoeling ;) ) een paar functies weggelaten. Daarom zie je niet waar reduce() aangeroepen wordt.

Als je geinteresseerd bent, hier een paar blogs die mij geholpen hebben te snappen hoe je interfaces/generics in TypeScript toe kunt passen:

https://www.typescriptlang.org/docs/handbook/generics.html
https://www.patrickdunn.org/advanced-types-in-typescript/
https://mariusschulz.com/...eneric-parameter-defaults

Acties:
  • +1 Henk 'm!

  • .oisyn
  • Registratie: September 2000
  • Laatst online: 21-08 11:35

.oisyn

Moderator Devschuur®

Demotivational Speaker

Ik heb inmiddels al wat meer gelezen en ik snap nu waarom het fout gaat :)

Je zit dus in een class Store<CONTEXT extends StoreContext, ACTION extends Action<CONTEXT>>. In de context van die class weet de compiler dus de volgende dingen:
CONTEXT is een type dat iig compatible is met StoreContext.
ACTION is een type dat iig compatible is met Action<CONTEXT>.

dispatch() vereist dat zijn parameter van het type ACTION is.

Maar wat CONTEXT en ACTION in werkelijkheid zijn, dat kan de compiler op dat moment niet weten. ACTION kan bijvoorbeeld een extra member vereisen. De objecten die je daar construct, als parameter voor dispatch(), voldoen in ieder geval aan Action<Context>, maar daarmee is niet gezegd dat ze ook aan de requirements van ACTION voldoen. Er is simpelweg geen manier voor de compiler om dat te controleren, omdat hij niet weet wat ACTION voorstelt.

Even een versimpeld voorbeeld (uit de losse pols met slechts enkele uren aan theoretische kennis over TypeScript, dus vergeef me eventuele fouten :P)

TypeScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Animal
{
    age: number;
}

interface Dog extends Animal
{
    breed: string;
}

class SomeClass<T extends Animal>
{
    private consume(param: T)
    {
    }

    public test()
    {
        this.consume({ age: 10 });
    }
}


Dit compilet als het goed is niet. Waarom niet? SomeClass.consume() verwacht een T. De compiler weet niet wat T is, maar het zal iig een afgeleide zijn van Animal. Wat in test() aan consume() wordt meegegeven zal in ieder geval voldoen aan Animal, maar dat is mogelijk niet genoeg, want hij verwacht een T.

Want stel nu dat je SomeClass<Dog> gebruikt. Dan verwacht consume() een Dog, maar wat test() daar construct voldoet niet aan een Dog: hij heeft geen member genaamd 'breed'.

.edit: ok klein foutje, maar nu lijkt de code zowaar te kloppen :P

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


Acties:
  • 0 Henk 'm!

  • TFBlunt
  • Registratie: November 2010
  • Laatst online: 08-08 10:27
Super bedankt!

Als ik het goed begrijp heb ik in mijn huidige class dus eigenlijk twee abstractie niveaus door elkaar lopen. Ik werk met een aanname over het operatietype die ik niet kan garanderen.

Als oplossing heb ik nu mijn class opgesplitst. Zie hieronder:
JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
export abstract class AbstractStore<CONTEXT extends StoreContext, ACTION extends Action<CONTEXT>> {

  private readonly actions$: Subject<ACTION> = new Subject<ACTION>();

  protected abstract reduce(state: CONTEXT, action: ACTION): CONTEXT;

  protected dispatch(action: ACTION): void {
    this.actions$.next(action);
  }
  
}

export abstract class BasicStore<CONTEXT> extends AbstractStore<CONTEXT, Action<CONTEXT>> {

  delete(options: { description?: string; path?: string[] } = {}): void {
    this.dispatch({ type: ActionType.DELETE, description: options.description, path: options.path });
  }

  set(options: { description?: string; payload?: CONTEXT } = {}): void {
    this.dispatch({ type: ActionType.SET, description: options.description, payload: options.payload });
  }

  update(options: { description?: string; path?: string[]; payload?: CONTEXT } = {}): void {
    this.dispatch({ type: ActionType.UPDATE, description: options.description, path: options.path, payload: options.payload });
  }

  protected reduce(state: CONTEXT, action: Action<CONTEXT>): CONTEXT {
    let next: CONTEXT;
    switch (action.type) {
      case ActionType.SET:
        next = action.payload;
        break;
      case ActionType.UPDATE:
        if (!action.path || action.path.length === 0) {
          next = Object.assign(state, action.payload);
        } else {
          next = setWith(clone(state), action.path, action.payload, clone);
        }
        break;
      case ActionType.DELETE:
        if (!action.path) {
          next = {} as CONTEXT;
        } else {
          next = omit(state, action.path);
        }
        break;
      default:
        next = state;
    }

    return next;
  }

Het complete voorbeeld: https://stackblitz.com/ed...ile=example%2Fsplit_up.ts

Wat nu graag nog zou willen is, wanneer ik in een subclass nieuwe operaties toevoeg, ik de reduce() functie uit mijn BasicStore (deels) kan hergebruiken. Dat kan nu niet, omdat BasicStore geen generiek operatietype meer gebruikt. Hoe zou ik dat aan kunnen pakken?