From a262305806f2169402d47eced71111af45d46b4c Mon Sep 17 00:00:00 2001 From: Javier Gonzalez Date: Tue, 4 May 2021 21:22:59 +0200 Subject: [PATCH] improved generics (#242) --- CHANGELOG.md | 2 + packages/lib/src/dataModel/DataModel.ts | 81 ++++++++++++--- packages/lib/src/model/Model.ts | 98 ++++++++++++++----- packages/lib/src/modelShared/prop.ts | 13 ++- packages/lib/test/dataModel/dataModel.test.ts | 32 ++++++ packages/lib/test/model/defaultProps.test.ts | 9 +- .../lib/test/model/modelDecorator.test.ts | 6 +- packages/lib/test/model/subclassing.test.ts | 78 +++++++++++---- packages/site/src/classModels.mdx | 27 ++++- packages/site/src/dataModels.mdx | 27 ++++- 10 files changed, 304 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22490685..819d810c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Change Log +- Added a simpler pattern for generic models when using `prop`. + ## 0.57.1 - Fixed an issue when importing the package with expo. diff --git a/packages/lib/src/dataModel/DataModel.ts b/packages/lib/src/dataModel/DataModel.ts index a1ec83d9..b7a4b237 100644 --- a/packages/lib/src/dataModel/DataModel.ts +++ b/packages/lib/src/dataModel/DataModel.ts @@ -2,25 +2,42 @@ import type { AbstractModelClass, ModelClass } from "../modelShared/BaseModelSha import { sharedInternalModel } from "../modelShared/Model" import type { ModelProps, ModelPropsToData, ModelPropsToSetter } from "../modelShared/prop" import type { AnyDataModel, BaseDataModel, BaseDataModelKeys } from "./BaseDataModel" -import { assertIsDataModelClass } from "./utils" +import { assertIsDataModelClass, isDataModelClass } from "./utils" -declare const dataSymbol: unique symbol - -declare const composedDataSymbol: unique symbol +export type _ComposedData = SuperModel extends BaseDataModel< + infer D +> + ? ModelPropsToData & D + : ModelPropsToData export interface _DataModel { - [dataSymbol]: ModelPropsToData - - [composedDataSymbol]: SuperModel extends BaseDataModel - ? this[typeof dataSymbol] & D - : this[typeof dataSymbol] - - new (data: this[typeof composedDataSymbol]): SuperModel & - BaseDataModel & - Omit & + new (data: _ComposedData): SuperModel & + BaseDataModel> & + Omit, BaseDataModelKeys> & ModelPropsToSetter } +/** + * Base abstract class for data models that extends another model. + * + * @typeparam TProps New model properties type. + * @typeparam TModel Model type. + * @param genFn Function that returns the base model and model properties. + * @returns + */ +export function ExtendedDataModel< + TProps extends ModelProps, + TModel extends AnyDataModel, + A extends [] +>( + genFn: ( + ...args: A + ) => { + baseModel: AbstractModelClass + props: TProps + } +): _DataModel + /** * Base abstract class for data models that extends another model. * @@ -33,12 +50,41 @@ export interface _DataModel { export function ExtendedDataModel( baseModel: AbstractModelClass, modelProps: TProps +): _DataModel + +// base +export function ExtendedDataModel( + ...args: any[] ): _DataModel { + let baseModel + let modelProps + if (isDataModelClass(args[0])) { + baseModel = args[0] + modelProps = args[1] + } else { + const gen = args[0]() + + baseModel = gen.baseModel + modelProps = gen.props + } + assertIsDataModelClass(baseModel, "baseModel") return internalDataModel(modelProps, baseModel as any) } +/** + * Base abstract class for data models. + * + * Never override the constructor, use `onLazyInit` or `onLazyAttachedToRootStore` instead. + * + * @typeparam TProps Model properties type. + * @param fnModelProps Function that generates model properties. + */ +export function DataModel( + fnModelProps: (...args: A) => TProps +): _DataModel + /** * Base abstract class for data models. * @@ -49,7 +95,16 @@ export function ExtendedDataModel( modelProps: TProps +): _DataModel + +// base +export function DataModel( + fnModelPropsOrModelProps: (() => TProps) | TProps ): _DataModel { + const modelProps = + typeof fnModelPropsOrModelProps === "function" + ? fnModelPropsOrModelProps() + : fnModelPropsOrModelProps return internalDataModel(modelProps, undefined) } diff --git a/packages/lib/src/model/Model.ts b/packages/lib/src/model/Model.ts index 9708b37d..e7bf2485 100644 --- a/packages/lib/src/model/Model.ts +++ b/packages/lib/src/model/Model.ts @@ -8,36 +8,23 @@ import { ModelPropsToSetter, } from "../modelShared/prop" import type { AnyModel, BaseModel, BaseModelKeys } from "./BaseModel" -import { modelTypeKey } from "./metadata" -import { assertIsModelClass } from "./utils" +import { assertIsModelClass, isModelClass } from "./utils" -declare const dataSymbol: unique symbol - -declare const creationDataSymbol: unique symbol - -declare const composedCreationDataSymbol: unique symbol +export type _ComposedCreationData< + SuperModel, + TProps extends ModelProps +> = SuperModel extends BaseModel + ? ModelPropsToCreationData & CD + : ModelPropsToCreationData export interface _Model { - /** - * Model type name assigned to this class, or undefined if none. - */ - readonly [modelTypeKey]: string | undefined - - [dataSymbol]: ModelPropsToData - - [creationDataSymbol]: ModelPropsToCreationData - - [composedCreationDataSymbol]: SuperModel extends BaseModel - ? this[typeof creationDataSymbol] & CD - : this[typeof creationDataSymbol] - - new (data: this[typeof composedCreationDataSymbol]): SuperModel & + new (data: _ComposedCreationData): SuperModel & BaseModel< - this[typeof dataSymbol], - this[typeof creationDataSymbol], + ModelPropsToData, + ModelPropsToCreationData, ExtractModelIdProp & string > & - Omit & + Omit, BaseModelKeys> & ModelPropsToSetter } @@ -58,6 +45,25 @@ export type ExtractModelIdProp = { [K in keyof TProps]: TProps[K]["$isId"] extends true ? K : never }[keyof TProps] +/** + * Base abstract class for models that extends another model. + * + * @typeparam TProps New model properties type. + * @typeparam TModel Model type. + * @param genFn Function that returns the base model and model properties. + * @param modelOptions Model options. + * @returns + */ +export function ExtendedModel( + genFn: ( + ...args: A + ) => { + baseModel: AbstractModelClass + props: TProps + }, + modelOptions?: ModelOptions +): _Model> + /** * Base abstract class for models that extends another model. * @@ -72,12 +78,46 @@ export function ExtendedModel, modelProps: TProps, modelOptions?: ModelOptions +): _Model> + +// base +export function ExtendedModel( + ...args: any[] ): _Model> { + let baseModel + let modelProps + let modelOptions + if (isModelClass(args[0])) { + baseModel = args[0] + modelProps = args[1] + modelOptions = args[2] + } else { + const gen = args[0]() + + baseModel = gen.baseModel + modelProps = gen.props + modelOptions = args[1] + } + assertIsModelClass(baseModel, "baseModel") return internalModel(modelProps, baseModel as any, modelOptions) } +/** + * Base abstract class for models. + * + * Never override the constructor, use `onInit` or `onAttachedToRootStore` instead. + * + * @typeparam TProps Model properties type. + * @param fnModelProps Function that generates model properties. + * @param modelOptions Model options. + */ +export function Model( + fnModelProps: (...args: A) => TProps, + modelOptions?: ModelOptions +): _Model> + /** * Base abstract class for models. * @@ -90,7 +130,17 @@ export function ExtendedModel( modelProps: TProps, modelOptions?: ModelOptions +): _Model> + +// base +export function Model( + fnModelPropsOrModelProps: (() => TProps) | TProps, + modelOptions?: ModelOptions ): _Model> { + const modelProps = + typeof fnModelPropsOrModelProps === "function" + ? fnModelPropsOrModelProps() + : fnModelPropsOrModelProps return internalModel(modelProps, undefined, modelOptions) } diff --git a/packages/lib/src/modelShared/prop.ts b/packages/lib/src/modelShared/prop.ts index 354e9b20..bc679db2 100644 --- a/packages/lib/src/modelShared/prop.ts +++ b/packages/lib/src/modelShared/prop.ts @@ -51,12 +51,19 @@ export type ModelPropsToData = { [k in keyof MP]: MP[k]["$valueType"] } -export type ModelPropsToCreationData = O.Optional< +// we don't use O.Optional anymore since it generates unions too heavy +export type ModelPropsToCreationData = O.Pick< { - [k in keyof MP]: MP[k]["$creationValueType"] + [k in keyof MP]?: MP[k]["$creationValueType"] }, OptionalModelProps -> +> & + O.Omit< + { + [k in keyof MP]: MP[k]["$creationValueType"] + }, + OptionalModelProps + > export type ModelPropsToSetter = { [k in keyof MP as MP[k]["$hasSetter"] & `set${Capitalize}`]: ( diff --git a/packages/lib/test/dataModel/dataModel.test.ts b/packages/lib/test/dataModel/dataModel.test.ts index 72ace6d0..6e39ba0b 100644 --- a/packages/lib/test/dataModel/dataModel.test.ts +++ b/packages/lib/test/dataModel/dataModel.test.ts @@ -10,6 +10,7 @@ import { Model, model, modelAction, + modelClass, ModelData, modelFlow, prop, @@ -957,3 +958,34 @@ test("extends works", () => { ] `) }) + +test("new pattern for generics", () => { + @model("GenericModel") + class GenericModel extends DataModel(() => ({ + v1: prop(), + v2: prop(), + v3: prop(), + })) {} + + assert(_ as ModelData>, _ as { v1: string; v2: number; v3: number }) + assert(_ as ModelData>, _ as { v1: number; v2: string; v3: number }) + + const s = new GenericModel({ v1: "1", v2: 2, v3: 3 }) + expect(s.v1).toBe("1") + expect(s.v2).toBe(2) + expect(s.v3).toBe(3) + + @model("ExtendedGenericModel") + class ExtendedGenericModel extends ExtendedDataModel(() => ({ + baseModel: modelClass>(GenericModel), + props: { + v4: prop(), + }, + })) {} + + const e = new ExtendedGenericModel({ v1: "1", v2: 2, v3: 3, v4: 4 }) + expect(e.v1).toBe("1") + expect(e.v2).toBe(2) + expect(e.v3).toBe(3) + expect(e.v4).toBe(4) +}) diff --git a/packages/lib/test/model/defaultProps.test.ts b/packages/lib/test/model/defaultProps.test.ts index 60c78639..fa6f5cf1 100644 --- a/packages/lib/test/model/defaultProps.test.ts +++ b/packages/lib/test/model/defaultProps.test.ts @@ -46,15 +46,16 @@ test("default props", () => { yy?: number | null yyy?: number | null - a: number aa?: number - aaa: number | null aaaa?: number | null - b: number bb?: number - bbb: number | null bbbb?: number | null + } & { + a: number + aaa: number | null + b: number + bbb: number | null } ) diff --git a/packages/lib/test/model/modelDecorator.test.ts b/packages/lib/test/model/modelDecorator.test.ts index b20b3a63..557b0cef 100644 --- a/packages/lib/test/model/modelDecorator.test.ts +++ b/packages/lib/test/model/modelDecorator.test.ts @@ -63,13 +63,13 @@ test("model decorator sets model type static prop and toString methods", () => { x: number = 1 // not-stored-properties not rendered } - expect(MyModel[modelTypeKey]).toBeUndefined() + expect((MyModel as any)[modelTypeKey]).toBeUndefined() const type = "com/myModel" const MyModel2 = model(type)(MyModel) - expect(MyModel[modelTypeKey]).toBe(type) - expect(MyModel2[modelTypeKey]).toBe(type) + expect((MyModel as any)[modelTypeKey]).toBe(type) + expect((MyModel2 as any)[modelTypeKey]).toBe(type) expect(`${MyModel}`).toBe(`class MyModel#${type}`) expect(`${MyModel2}`).toBe(`class MyModel#${type}`) diff --git a/packages/lib/test/model/subclassing.test.ts b/packages/lib/test/model/subclassing.test.ts index 0635c3d0..94322a49 100644 --- a/packages/lib/test/model/subclassing.test.ts +++ b/packages/lib/test/model/subclassing.test.ts @@ -1,5 +1,6 @@ import { computed } from "mobx" import { assert, _ } from "spec.ts" +import { O } from "ts-toolbelt" import { ExtendedModel, fromSnapshot, @@ -42,6 +43,8 @@ class P extends Model({ } } +type Empty = O.Omit<{}, ""> + test("subclassing with additional props", () => { @model("P2_props") class P2 extends ExtendedModel(P, { @@ -84,11 +87,12 @@ test("subclassing with additional props", () => { x?: number | null y?: number | null z?: number | null - } & { - [modelIdKey]?: string - a?: number | null - b: number - } + } & Empty & { + [modelIdKey]?: string + a?: number | null + } & { + b: number + } ) const p2 = new P2({ x: 20, b: 70 }) @@ -160,9 +164,9 @@ test("subclassing without additional props", () => { x?: number | null y?: number | null z?: number | null - } & { - [modelIdKey]?: string - } + } & Empty & { + [modelIdKey]?: string + } & Empty ) const p2 = new P2({ x: 20 }) @@ -219,9 +223,9 @@ test("subclassing without anything new", () => { x?: number | null y?: number | null z?: number | null - } & { - [modelIdKey]?: string - } + } & Empty & { + [modelIdKey]?: string + } & Empty ) const p2 = new P2({ x: 20 }) @@ -308,13 +312,14 @@ test("three level subclassing", () => { x?: number | null | undefined y?: number | null | undefined z?: number | null | undefined - } & { - [modelIdKey]?: string - a?: number | null | undefined - } & { - [modelIdKey]?: string - b: number - } + } & Empty & { + [modelIdKey]?: string + a?: number | null | undefined + } & { + [modelIdKey]?: string + } & { + b: number + } ) const p2 = new P2({ x: 20, b: 70 }) @@ -703,3 +708,40 @@ test("ExtendedModel should bring static / prototype properties", () => { expect((extendedBobbin as any).first2).toBe("Guybrush") expect((extendedBobbin as any).LAST2).toBe(undefined) }) + +test("new pattern for generics", () => { + @model("GenericModel") + class GenericModel extends Model(() => ({ + v1: prop(), + v2: prop(), + v3: prop(0), + })) {} + + assert( + _ as ModelData>, + _ as { [modelIdKey]: string; v1: string; v2: number; v3: number } + ) + assert( + _ as ModelData>, + _ as { [modelIdKey]: string; v1: number; v2: string; v3: number } + ) + + const s = new GenericModel({ v1: "1", v2: 2, v3: 3 }) + expect(s.v1).toBe("1") + expect(s.v2).toBe(2) + expect(s.v3).toBe(3) + + @model("ExtendedGenericModel") + class ExtendedGenericModel extends ExtendedModel(() => ({ + baseModel: modelClass>(GenericModel), + props: { + v4: prop(), + }, + })) {} + + const e = new ExtendedGenericModel({ v1: "1", v2: 2, v3: 3, v4: 4 }) + expect(e.v1).toBe("1") + expect(e.v2).toBe(2) + expect(e.v3).toBe(3) + expect(e.v4).toBe(4) +}) diff --git a/packages/site/src/classModels.mdx b/packages/site/src/classModels.mdx index f22820b2..aa7fe644 100644 --- a/packages/site/src/classModels.mdx +++ b/packages/site/src/classModels.mdx @@ -335,9 +335,32 @@ myColors.setPrimary(myColors.secondary) Note that it is an actual clone, this is, changing the primary color won't change the secondary one. -## Factory pattern +## Factory pattern / Generics -It is possible to use a factory pattern with class models. For example: +If you are _not_ relying on `tProp` to do runtime type-checking it is possible to use this pattern to get generic classes: + +```ts +@model("myApp/GenericPoint") +class GenericPoint extends Model(() => ({ + x: prop(), + y: prop(), +})) { + @modelAction + setXY(x: T, y: T) { + this.x = x + this.y = y + } +} + +@model("myApp/Generic3dPoint") +class Generic3dPoint extends ExtendedModel(() => ({ + z: prop(), +})) { + // ... +} +``` + +If you rely on `tProp` (and also `prop` really) a different possibility is to use a factory pattern with class models. For example: ```ts function createModelClass(modelName: string, initialX: TX, initialY: TY) { diff --git a/packages/site/src/dataModels.mdx b/packages/site/src/dataModels.mdx index f4b6206b..d4daf1bd 100644 --- a/packages/site/src/dataModels.mdx +++ b/packages/site/src/dataModels.mdx @@ -232,9 +232,32 @@ const myBookStore = new BookStore({}) await myBookStore.fetchMyBooksAsync("someToken") ``` -## Factory pattern +## Factory pattern / Generics -It is possible to use a factory pattern with data models. For example: +If you are _not_ relying on `tProp` to do runtime type-checking it is possible to use this pattern to get generic classes: + +```ts +@model("myApp/GenericPoint") +class GenericPoint extends DataModel(() => ({ + x: prop(), + y: prop(), +})) { + @modelAction + setXY(x: T, y: T) { + this.x = x + this.y = y + } +} + +@model("myApp/Generic3dPoint") +class Generic3dPoint extends ExtendedDataModel(() => ({ + z: prop(), +})) { + // ... +} +``` + +If you rely on `tProp` (and also `prop` really) a different possibility is to use a factory pattern with data models. For example: ```ts function createModelClass(modelName: string, initialX: TX, initialY: TY) {