Skip to content

useStoreon hook types are misleading and can lead to runtime errors #154

@SeinopSys

Description

@SeinopSys

Steps to reproduce

Consider the following state:

interface AppState {
  someString: string;
  someBoolean: boolean;
  partialObject: Partial<{ x: number; y: number }>;
  anotherBoolean: boolean;
  anotherString: string;
}

export interface AppEvents {
  setValue: string;
}

If you plug this into the useStoreon hook to use in your component, it might look something like this:

import { FunctionComponent, PropsWithChildren } from 'react';
import { useStoreon } from 'storeon/react';
import { AppEvents, AppState } from './app.state';

const MyComponent: FunctionComponent<PropsWithChildren> = ({ children }) => {
  const {
    someString,
    someBoolean,
    anotherBoolean,
    anotherString
  } = useStoreon<AppState, AppEvents>('someString','someBoolean','anotherBoolean');

  const attributeValue = someString.length > 0 && anotherString.length > 0 && someBoolean && !anotherBoolean ? 'value1' : 'value2';

  return <div data-tranformed-attribute={attributeValue}>{children}</div>
};

This code compiles perfectly fine and reports absolutely no type issues. However, as soon as you actually try to use the component with a perfectly valid state, you get the following error:

Uncaught TypeError: anotherString is undefined

Those deeply familiar with the library might have caught the fact that 'anotherString' was missing from the useStoreon hook call parameters, which is causing the value that would otherwise always be a string to return undefined, breaking the code during runtime.

Fix ideas

Some libraries make use of function overloads to achieve the desired outcome, this does require however that the generic type parameters be handled differently:

// Without parameters, no property can possibly be returned
export function useStoreon<State, Events>(): Record<string, undefined>;
// Adding overloads for individual parameters
export function useStoreon<State, Events, Key1 extends keyof State>(key1: Key1): Pick<State, Key1>;
export function useStoreon<State, Events, Key1 extends keyof State, Key2 extends keyof State>(key1: Key1, key2: Key2): Pick<State, Key1 | Key2>;
// etc.

This does have the drawback however that you would manually have to duplicate each key in both the type arguments as well as the function properties. A potential alternative would be to change the function signature to return a new function which is fully type safe without having to specify more than the state and event types:

export function createUseStoreon<State, Events>():
  | (<Key1 extends keyof State>(key1: Key1) => Pick<State, Key1>)
  | (<Key1 extends keyof State, Key2 extends keyof State>(key1: Key1, key2: Key2) => Pick<State, Key1 | Key2>);

I'm not really sure if there is a better way, but this is quite a frustrating issue that has caused me a lot of unnecessary debugging time and I would very much appreciate if this could be addressed somehow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions