Easy-peasy: [Proposal] - Supporting Generic properties in model

Created on 13 Nov 2019  路  8Comments  路  Source: ctrlplusb/easy-peasy

Sorry if this is a duplicate.

interface StoreModel<K extends {}> {
  data: K;
  act: Action<StoreModel<K>, void>;
}

function doSomething<K extends {}, S extends StoreModel<K>>(
  s: State<S>,
  a: Actions<S>
) {
  a.act();
  return s.data;
}

According to Typescript, act and data don't exist on a and s, respectively.

typescript

Most helpful comment

I might have a solution for this. Will keep u posted 馃憤

All 8 comments

Hey @ae2438

Unfortunately I unable to resolve the case of generic properties due to current limitations with the TypeScript type system.

I created a StackOverflow question which details the problem.

In a gist; the issue is that the State and Action types map over the properties of the incoming model types and then remove the types that do not belong to their specific cases. The problem with this when it comes to generics is that TypeScript will assume that an unconstrained generic essentially intersects with the types that are trying to be filtered off.

It would be great if TypeScript allowed me to express a negative contstraint.

e.g.

interface StoreModel<K extends {} except Actions> {
  data: K;
  act: Action<StoreModel<K>, void>;
}

To workaround this limitation I recommend creating a value-object wrapper.

For e.g.

import { Actions, State, Action } from "easy-peasy";

type Value<V> = [V];

interface StoreModel<K extends {}> {
  data: Value<K>;
  act: Action<StoreModel<K>, void>;
}

function doSomething<K extends {}>(
  s: State<StoreModel<K>>,
  a: Actions<StoreModel<K>>
): K {
  a.act();
  return s.data[0];
}

I might have a solution for this. Will keep u posted 馃憤

Cheers Sean! Great response time as always :)

For now I've limited the extending functionality to just overriding existing actions, reducing doSomething to just

function doSomething<K extends {}>(
  s: State<StoreModel<K>>,
  a: Actions<StoreModel<K>>
) {
  a.act();
  return s.data;
}

which does the trick but isn't ideal. The top answer on StackOverflow was an adventure, hopefully you can work that into a solution :D

@williammetcalf @ae2438 @djgrant

So I was finally able to parse the information from the answer to my StackOverflow question on this subject. I spent a lot of time on this and got to go one level deeper in terms of my level of understanding in regards to the TypeScript compiler.

I won't go into the specifics, but basically I was hacking around some ugliness of the internals of the TS compiler.

I _almost_ got a working solution, however, I eventually hit a dead end in which I created a see-saw effect. If I fixed the mapping of generic properties I would lose the ability to map other types (such as computed properties, reducers etc).

After much more experimentation and Google / StackOverflow reading I am now convinced that a full solution for mapping generics in EP models is not possible, given the current limitations of the TS compiler.

All is not lost though. I have come up with a workaround. What do you all think of the below idea of introducing a Generic / generic pair of helpers, which only need to be used when declaring generic properties on a model.

import { Generic, generic, Action } from 'easy-peasy';

interface StoreModel<T> {
  // Any of your generic properties should be wrapped
  // in the `Generic` type. This provides the handle I 
  // need as a workaround to the TS compiler limitations.
  //            馃憞
  value: Generic<T>; 
  // Anywhere else you just declare the T as is. No need
  // to wrap in the Generic helper. Only the properties
  // themselves would need to be wrapped.
  //                         馃憞  馃憞
  setValue: Action<StoreModel<T>, T>;
}

function makeModel<T>(value: T): StoreModel<T> {
  return {
    // Surround the assigning of your generic property
    // with the "generic" helper. This is really a no-op 
    // under the hood. The actual implementation will
    // just return the provided value, however, it gives
    // the opportunity for me to create a handle / workaround
    // for the TS compiler.
    //              馃憞
    value: generic(value), 
    setValue: action((state, payload) => {
      // 鈩癸笍 In here the "payload" will be of type "T".
      // You can just use it as the value you would have
      // have expected it to be.
      state.value = payload;
    }),
  };
}

const numberModel = makeModel<number>(1337);

const store = createStore(numberModel);

// Just passing in the actual value here. No need to wrap with "generic" helper.
//                           馃憞
store.getActions().setValue(777);

const typedHooks = createTypedHooks<typeof numberModel>();

function MyComponent() {
  const value = typedHooks.useStoreState(state => state.value);
  //     馃憜 value is of type "T", in our example case a "number". No unwrapping
  //        of the value would be required.
}

Thoughts?

Ah that's a shame the compiler wouldn't play ball. Hope you gleaned some useful learnings along the way for all your efforts.

The proposal seems like a reasonable compromise to me! The main thing IMO is to make sure it's clear in the TypeScript section of the documentation as you wouldn't guess you need to do something like this and it's only really a pain point the first time you fail to get your code to compile.

Will there be an error if you set a property field to an unwrapped generic type? That would certainly help point people in the right direction

Will there be an error if you set a property field to an unwrapped generic type? That would certainly help point people in the right direction

I'll investigate the possibility of this utilising some of the trickery I caught on to. My gut says unlikely though.

I'll try to make this gotcha/workaround as prominent as possible in the docs.

Luckily it seems not too many people have hit this issue. Seems to be isolated power user cases right now.

I was so close though. Really frustrating in the end. 馃槪

This seems like a good solution! Sad the preferred solution is currently not possible due to the typescript engine.

Was this page helpful?
0 / 5 - 0 ratings