Skip to content

Commit

Permalink
improved generics
Browse files Browse the repository at this point in the history
  • Loading branch information
xaviergonz committed May 3, 2021
1 parent e77e359 commit ff21f7b
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 69 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
81 changes: 68 additions & 13 deletions packages/lib/src/dataModel/DataModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, TProps extends ModelProps> = SuperModel extends BaseDataModel<
infer D
>
? ModelPropsToData<TProps> & D
: ModelPropsToData<TProps>

export interface _DataModel<SuperModel, TProps extends ModelProps> {
[dataSymbol]: ModelPropsToData<TProps>

[composedDataSymbol]: SuperModel extends BaseDataModel<infer D>
? this[typeof dataSymbol] & D
: this[typeof dataSymbol]

new (data: this[typeof composedDataSymbol]): SuperModel &
BaseDataModel<this[typeof dataSymbol]> &
Omit<this[typeof dataSymbol], BaseDataModelKeys> &
new (data: _ComposedData<SuperModel, TProps>): SuperModel &
BaseDataModel<ModelPropsToData<TProps>> &
Omit<ModelPropsToData<TProps>, BaseDataModelKeys> &
ModelPropsToSetter<TProps>
}

/**
* 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<TModel>
props: TProps
}
): _DataModel<TModel, TProps>

/**
* Base abstract class for data models that extends another model.
*
Expand All @@ -33,12 +50,41 @@ export interface _DataModel<SuperModel, TProps extends ModelProps> {
export function ExtendedDataModel<TProps extends ModelProps, TModel extends AnyDataModel>(
baseModel: AbstractModelClass<TModel>,
modelProps: TProps
): _DataModel<TModel, TProps>

// base
export function ExtendedDataModel<TProps extends ModelProps, TModel extends AnyDataModel>(
...args: any[]
): _DataModel<TModel, TProps> {
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<TProps extends ModelProps, A extends []>(
fnModelProps: (...args: A) => TProps
): _DataModel<unknown, TProps>

/**
* Base abstract class for data models.
*
Expand All @@ -49,7 +95,16 @@ export function ExtendedDataModel<TProps extends ModelProps, TModel extends AnyD
*/
export function DataModel<TProps extends ModelProps>(
modelProps: TProps
): _DataModel<unknown, TProps>

// base
export function DataModel<TProps extends ModelProps>(
fnModelPropsOrModelProps: (() => TProps) | TProps
): _DataModel<unknown, TProps> {
const modelProps =
typeof fnModelPropsOrModelProps === "function"
? fnModelPropsOrModelProps()
: fnModelPropsOrModelProps
return internalDataModel(modelProps, undefined)
}

Expand Down
98 changes: 74 additions & 24 deletions packages/lib/src/model/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any, infer CD, any>
? ModelPropsToCreationData<TProps> & CD
: ModelPropsToCreationData<TProps>

export interface _Model<SuperModel, TProps extends ModelProps> {
/**
* Model type name assigned to this class, or undefined if none.
*/
readonly [modelTypeKey]: string | undefined

[dataSymbol]: ModelPropsToData<TProps>

[creationDataSymbol]: ModelPropsToCreationData<TProps>

[composedCreationDataSymbol]: SuperModel extends BaseModel<any, infer CD, any>
? this[typeof creationDataSymbol] & CD
: this[typeof creationDataSymbol]

new (data: this[typeof composedCreationDataSymbol]): SuperModel &
new (data: _ComposedCreationData<SuperModel, TProps>): SuperModel &
BaseModel<
this[typeof dataSymbol],
this[typeof creationDataSymbol],
ModelPropsToData<TProps>,
ModelPropsToCreationData<TProps>,
ExtractModelIdProp<TProps> & string
> &
Omit<this[typeof dataSymbol], BaseModelKeys> &
Omit<ModelPropsToData<TProps>, BaseModelKeys> &
ModelPropsToSetter<TProps>
}

Expand All @@ -58,6 +45,25 @@ export type ExtractModelIdProp<TProps extends ModelProps> = {
[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<TProps extends ModelProps, TModel extends AnyModel, A extends []>(
genFn: (
...args: A
) => {
baseModel: AbstractModelClass<TModel>
props: TProps
},
modelOptions?: ModelOptions
): _Model<TModel, AddModelIdPropIfNeeded<TProps>>

/**
* Base abstract class for models that extends another model.
*
Expand All @@ -72,12 +78,46 @@ export function ExtendedModel<TProps extends ModelProps, TModel extends AnyModel
baseModel: AbstractModelClass<TModel>,
modelProps: TProps,
modelOptions?: ModelOptions
): _Model<TModel, AddModelIdPropIfNeeded<TProps>>

// base
export function ExtendedModel<TProps extends ModelProps, TModel extends AnyModel>(
...args: any[]
): _Model<TModel, AddModelIdPropIfNeeded<TProps>> {
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<TProps extends ModelProps, A extends []>(
fnModelProps: (...args: A) => TProps,
modelOptions?: ModelOptions
): _Model<unknown, AddModelIdPropIfNeeded<TProps>>

/**
* Base abstract class for models.
*
Expand All @@ -90,7 +130,17 @@ export function ExtendedModel<TProps extends ModelProps, TModel extends AnyModel
export function Model<TProps extends ModelProps>(
modelProps: TProps,
modelOptions?: ModelOptions
): _Model<unknown, AddModelIdPropIfNeeded<TProps>>

// base
export function Model<TProps extends ModelProps>(
fnModelPropsOrModelProps: (() => TProps) | TProps,
modelOptions?: ModelOptions
): _Model<unknown, AddModelIdPropIfNeeded<TProps>> {
const modelProps =
typeof fnModelPropsOrModelProps === "function"
? fnModelPropsOrModelProps()
: fnModelPropsOrModelProps
return internalModel(modelProps, undefined, modelOptions)
}

Expand Down
13 changes: 10 additions & 3 deletions packages/lib/src/modelShared/prop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,19 @@ export type ModelPropsToData<MP extends ModelProps> = {
[k in keyof MP]: MP[k]["$valueType"]
}

export type ModelPropsToCreationData<MP extends ModelProps> = O.Optional<
// we don't use O.Optional anymore since it generates unions too heavy
export type ModelPropsToCreationData<MP extends ModelProps> = O.Pick<
{
[k in keyof MP]: MP[k]["$creationValueType"]
[k in keyof MP]?: MP[k]["$creationValueType"]
},
OptionalModelProps<MP>
>
> &
O.Omit<
{
[k in keyof MP]: MP[k]["$creationValueType"]
},
OptionalModelProps<MP>
>

export type ModelPropsToSetter<MP extends ModelProps> = {
[k in keyof MP as MP[k]["$hasSetter"] & `set${Capitalize<k & string>}`]: (
Expand Down
32 changes: 32 additions & 0 deletions packages/lib/test/dataModel/dataModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Model,
model,
modelAction,
modelClass,
ModelData,
modelFlow,
prop,
Expand Down Expand Up @@ -957,3 +958,34 @@ test("extends works", () => {
]
`)
})

test("new pattern for generics", () => {
@model("GenericModel")
class GenericModel<T1, T2> extends DataModel(<U1, U2>() => ({
v1: prop<U1>(),
v2: prop<U2>(),
v3: prop<number>(),
}))<T1, T2> {}

assert(_ as ModelData<GenericModel<string, number>>, _ as { v1: string; v2: number; v3: number })
assert(_ as ModelData<GenericModel<number, string>>, _ as { v1: number; v2: string; v3: number })

const s = new GenericModel<string, number>({ 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<T1, T2> extends ExtendedDataModel(<T1, T2>() => ({
baseModel: modelClass<GenericModel<T1, T2>>(GenericModel),
props: {
v4: prop<T2>(),
},
}))<T1, T2> {}

const e = new ExtendedGenericModel<string, number>({ 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)
})
9 changes: 5 additions & 4 deletions packages/lib/test/model/defaultProps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
)

Expand Down
6 changes: 3 additions & 3 deletions packages/lib/test/model/modelDecorator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand Down
Loading

0 comments on commit ff21f7b

Please sign in to comment.