From 7ce3188294774785555a99e8b14881267700cd26 Mon Sep 17 00:00:00 2001 From: Sean Matheson Date: Wed, 3 Jul 2019 21:30:12 +0100 Subject: [PATCH] Fixes index signatures on state --- index.d.ts | 60 +++++++++++++---------- package.json | 2 +- src/__tests__/typescript/generic-model.ts | 57 +++++++++++++++++++++ src/__tests__/typescript/issue224.ts | 44 +++++++---------- 4 files changed, 110 insertions(+), 53 deletions(-) create mode 100644 src/__tests__/typescript/generic-model.ts diff --git a/index.d.ts b/index.d.ts index 33afe5cbd..70e6445fc 100644 --- a/index.d.ts +++ b/index.d.ts @@ -61,7 +61,7 @@ export function thunkFailName(action: Thunk): string; // #region Actions -type ActionMapper = { +type ActionMapper = { [P in keyof ActionsModel]: ActionsModel[P] extends Thunk< any, any, @@ -77,23 +77,23 @@ type ActionMapper = { : ActionsModel[P] extends object ? RecursiveActions< ActionsModel[P], - Depth extends 1 - ? 2 - : Depth extends 2 - ? 3 - : Depth extends 3 - ? 4 - : Depth extends 4 - ? 5 - : 6 + Depth extends '1' + ? '2' + : Depth extends '2' + ? '3' + : Depth extends '3' + ? '4' + : Depth extends '4' + ? '5' + : '6' > : unknown; }; type RecursiveActions< Model extends Object, - Depth extends number -> = Depth extends 6 + Depth extends string +> = Depth extends '6' ? Model : ActionMapper< O.Filter< @@ -114,13 +114,13 @@ type RecursiveActions< * * type OnlyActions = Actions; */ -export type Actions = RecursiveActions; +export type Actions = RecursiveActions; // #endregion // #region State -type StateMapper = { +type StateMapper = { [P in keyof StateModel]: StateModel[P] extends Computed ? StateModel[P]['result'] : StateModel[P] extends Reducer @@ -130,25 +130,31 @@ type StateMapper = { ? StateModel[P] : RecursiveState< StateModel[P], - Depth extends 1 - ? 2 - : Depth extends 2 - ? 3 - : Depth extends 3 - ? 4 - : Depth extends 4 - ? 5 - : 6 + Depth extends '1' + ? '2' + : Depth extends '2' + ? '3' + : Depth extends '3' + ? '4' + : Depth extends '4' + ? '5' + : '6' > : StateModel[P]; }; type RecursiveState< Model extends object, - Depth extends number -> = Depth extends 6 + Depth extends string +> = Depth extends '6' ? Model - : StateMapper, Depth>; + : O.Merge< + StateMapper< + O.Omit, IndexSignatureKeysOfType>, + Depth + >, + O.Pick> + >; /** * Filters a model into a type that represents the state only (i.e. no actions) @@ -157,7 +163,7 @@ type RecursiveState< * * type StateOnly = State; */ -export type State = RecursiveState; +export type State = RecursiveState; // #endregion diff --git a/package.json b/package.json index c2d17a18d..00feb1c9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "easy-peasy", - "version": "2.6.6", + "version": "3.0.0-alpha.0", "description": "Easy peasy state for React", "license": "MIT", "main": "dist/easy-peasy.cjs.js", diff --git a/src/__tests__/typescript/generic-model.ts b/src/__tests__/typescript/generic-model.ts new file mode 100644 index 000000000..fc53f0c5f --- /dev/null +++ b/src/__tests__/typescript/generic-model.ts @@ -0,0 +1,57 @@ +import { O } from 'ts-toolbelt'; + +/** + * ARRANGE + */ + +interface Person { + name: string; +} + +type StateMapper = { + [P in keyof Model]: Model[P]; +}; + +type State = StateMapper>>; + +type Action = (state: State) => void; + +/** + * WORKING CASE - "NORMAL" MODEL INTERFACE + */ + +interface NormalModel { + data: Person[]; + fetch: Action; +} + +const normalModel: NormalModel = { + data: [], + fetch: state => { + // 👍 works + state.data[0].name; + }, +}; + +/** + * BROKEN CASE - "GENERIC" MODEL INTERFACE + */ + +interface ObjectWithId { + id: string; +} + +interface GenericModel { + data: Item[]; + fetch: Action>; +} + +const createModel = (): GenericModel => { + return { + data: [], + fetch: state => { + // 👇 broken + state.data; + }, + }; +}; diff --git a/src/__tests__/typescript/issue224.ts b/src/__tests__/typescript/issue224.ts index 82a7d1a98..afcb22365 100644 --- a/src/__tests__/typescript/issue224.ts +++ b/src/__tests__/typescript/issue224.ts @@ -11,23 +11,23 @@ import { } from 'easy-peasy'; interface ObjectWithId { - id: number; + id: string; } interface Nested { - save: Thunk; + save: Thunk; } interface DataModel { - data: Record; - sortBy: keyof DataItem | 'none'; + data: { [key: number]: DataItem }; + sortBy: 'none' | string; name: string; - ids: Computed, string[]>; + ids: Computed, number[]>; fetched: Action, DataItem[]>; fetch: Thunk, string>; getItemById: Computed< DataModel, - (id: number) => DataItem | undefined + (id: string) => DataItem | undefined >; nested: Nested; } @@ -35,52 +35,46 @@ interface DataModel { const dataModel = ( name: string, endpoint: () => Promise, - // @ts-ignore ): DataModel => { - /* - return { + const result: DataModel = { data: {}, + sortBy: 'none', name, - ids: computed(state => Object.keys(state.data)), + ids: computed(state => Object.keys(state.data).map(id => parseInt(id))), fetched: action((state, items) => { + state.name; items.forEach((item, idx) => { state.data[idx] = item; }); - 84656; }), fetch: thunk(async (actions, payload) => { const data = await endpoint(); actions.fetched(data); - // Nested actions do not work on generics :( - // typings:expect-error - actions.nested.save(); + actions.nested.save(1); }), - getItemById: computed(state => (id: number) => + getItemById: computed(state => (id: string) => Object.values(state.data).find(item => item.id === id), ), - sortBy: 'id', nested: { - save: thunk(() => {}), + save: thunk((actions, payload) => { + actions.save(payload + 1); + }), }, }; - */ + return result; }; interface Person extends ObjectWithId { - id: number; + id: string; name: string; } const personModel = dataModel('person', () => - Promise.resolve([{ id: 1, name: 'bob' }]), + Promise.resolve([{ id: '1', name: 'bob' }]), ); const store = createStore(personModel); -store.getState().sortBy; - store.getActions().fetched([]); store.getActions().data; -// typings:expect-error -store.getActions().sortBy; -store.getActions().nested.save(); +store.getActions().nested.save(1);