Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix model types when extending in any way. #2218

Merged
merged 1 commit into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 104 additions & 24 deletions __tests__/core/type-system.test.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,37 @@
import { describe, expect, test } from "bun:test"
coolsoftwaretyler marked this conversation as resolved.
Show resolved Hide resolved
import {
types,
getSnapshot,
unprotect,
getRoot,
getParent,
SnapshotOrInstance,
cast,
SnapshotIn,
Instance,
castToSnapshot,
IType,
isStateTreeNode,
isFrozenType,
TypeOfValue,
IAnyType,
IType,
Instance,
ModelPrimitive,
ModelPropertiesDeclaration,
SnapshotIn,
SnapshotOrInstance,
SnapshotOut,
type ISimpleType,
isOptionalType,
isUnionType,
type IOptionalIType,
type ITypeUnion,
isMapType,
TypeOfValue,
cast,
castToSnapshot,
getParent,
getRoot,
getSnapshot,
isArrayType,
isModelType,
isFrozenType,
isIdentifierType,
isLateType,
isLiteralType,
isMapType,
isModelType,
isOptionalType,
isPrimitiveType,
isReferenceType,
isIdentifierType,
isRefinementType,
isLateType
isStateTreeNode,
isUnionType,
types,
unprotect,
type IOptionalIType,
type ISimpleType
} from "../../src"
import { describe, expect, test } from "bun:test"
import type {
DatePrimitive,
IAnyComplexType,
Expand Down Expand Up @@ -1403,3 +1402,84 @@ test("#1627 - union dispatch function is typed", () => {
types.null
)
})

test("#2216 - should respect optionality when extending another type", () => {
const Base = types.model("ErrorStore", { value: types.string }).extend((self) => ({
actions: {
setValue(value?: string): boolean {
self.value = value || "test"
return true
},

setAnotherValue(value?: string): boolean {
self.value = value || "test"
return true
}
},
views: {
get spam(): string {
return self.value
},

get eggs(): string {
return self.value
}
},
state: {
anotherValue: "test" as string,
soManyValues: "test" as string
Comment on lines +1429 to +1430
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: many values, very wow.

}
}))

const Extended = Base.named("Extended")
.props({
value: "test"
})
.extend((self) => ({
actions: {
setValue(value: string): number {
self.value = value
return value.length
}
},
views: {
get spam(): boolean {
return !!self.value
}
},
state: {
anotherValue: "test" as string | undefined
}
}))
.actions((self) => ({
setAnotherValue(value: string): number {
self.value = value
return value.length
}
}))
.views((self) => ({
get eggs(): boolean {
return !!self.value
}
}))
.volatile((self) => ({
soManyValues: "test" as string | undefined
}))

type InputSnapshot = SnapshotIn<typeof Extended>
type InstanceType = Instance<typeof Extended>

assertTypesEqual(_ as InputSnapshot, _ as { value?: string })
assertTypesEqual(
_ as InstanceType,
_ as {
value: string
setValue(value: string): number
setAnotherValue(value: string): number
spam: boolean
eggs: boolean
anotherValue: string | undefined
soManyValues: string | undefined
}
)
})
96 changes: 53 additions & 43 deletions src/types/complex-types/model.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,60 @@
import {
IObjectDidChange,
IObjectWillChange,
_getAdministration,
_interceptReads,
action,
computed,
defineProperty,
intercept,
getAtom,
IObjectWillChange,
intercept,
makeObservable,
observable,
observe,
set,
IObjectDidChange,
makeObservable
set
} from "mobx"
import {
addHiddenFinalProp,
addHiddenWritableProp,
AnyNode,
AnyObjectNode,
ArrayType,
ComplexType,
createActionInvoker,
createObjectNode,
EMPTY_ARRAY,
EMPTY_OBJECT,
escapeJsonPath,
FunctionWithFlag,
Hook,
IAnyType,
IChildNodesMap,
IJsonPatch,
IType,
IValidationContext,
IValidationResult,
Instance,
MapType,
MstError,
TypeFlags,
_CustomOrOther,
_NotCustomized,
addHiddenFinalProp,
addHiddenWritableProp,
assertArg,
assertIsString,
createActionInvoker,
createObjectNode,
devMode,
escapeJsonPath,
flattenTypeErrors,
freeze,
getContextForPath,
getPrimitiveFactoryFromValue,
getStateTreeNode,
IAnyType,
IChildNodesMap,
IValidationContext,
IJsonPatch,
isPlainObject,
isPrimitive,
isStateTreeNode,
isType,
IType,
IValidationResult,
mobxShallow,
optional,
MapType,
typecheckInternal,
typeCheckFailure,
TypeFlags,
Hook,
AnyObjectNode,
AnyNode,
_CustomOrOther,
_NotCustomized,
Instance,
devMode,
assertIsString,
assertArg,
FunctionWithFlag,
type IStateTreeNode
typecheckInternal
} from "../../internal"

const PRE_PROCESS_SNAPSHOT = "preProcessSnapshot"
Expand All @@ -74,6 +73,9 @@ export interface ModelPropertiesDeclaration {
[key: string]: ModelPrimitive | IAnyType
}

/** intersect two object types, but omit keys of B from A before doing so */
type OmitMerge<A, B> = Omit<A, keyof B> & B
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: NICE


/**
* Unmaps syntax property declarations to a map of { propName: IType }
*
Expand Down Expand Up @@ -117,6 +119,8 @@ type IsOptionalValue<C, TV, FV> = undefined extends C ? TV : FV
// type _E = IsOptionalValue<any, true, false> // true
// type _F = IsOptionalValue<unknown, true, false> // true

type AnyObject = Record<string, any>

/**
* Name of the properties of an object that can't be set to undefined, any or unknown
* @hidden
Expand Down Expand Up @@ -199,23 +203,28 @@ export interface IModelType<
// so it is recommended to use pre/post process snapshot after all props have been defined
props<PROPS2 extends ModelPropertiesDeclaration>(
props: PROPS2
): IModelType<PROPS & ModelPropertiesDeclarationToProperties<PROPS2>, OTHERS, CustomC, CustomS>
): IModelType<
OmitMerge<PROPS, ModelPropertiesDeclarationToProperties<PROPS2>>,
OTHERS,
CustomC,
CustomS
>

views<V extends Object>(
views<V extends AnyObject>(
fn: (self: Instance<this>) => V
): IModelType<PROPS, OTHERS & V, CustomC, CustomS>
): IModelType<PROPS, OmitMerge<OTHERS, V>, CustomC, CustomS>

actions<A extends ModelActions>(
fn: (self: Instance<this>) => A
): IModelType<PROPS, OTHERS & A, CustomC, CustomS>
): IModelType<PROPS, OmitMerge<OTHERS, A>, CustomC, CustomS>

volatile<TP extends object>(
fn: (self: Instance<this>) => TP
): IModelType<PROPS, OTHERS & TP, CustomC, CustomS>
volatile<VS extends AnyObject>(
fn: (self: Instance<this>) => VS
): IModelType<PROPS, OmitMerge<OTHERS, VS>, CustomC, CustomS>

extend<A extends ModelActions = {}, V extends Object = {}, VS extends Object = {}>(
extend<A extends ModelActions = {}, V extends AnyObject = {}, VS extends AnyObject = {}>(
fn: (self: Instance<this>) => { actions?: A; views?: V; state?: VS }
): IModelType<PROPS, OTHERS & A & V & VS, CustomC, CustomS>
): IModelType<PROPS, OmitMerge<OTHERS, A & V & VS>, CustomC, CustomS>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the fix for the issue, but I changed the above to resolve a similar issue.

Technically we likely want to deal with this in compose too, but I'd rather deal with that when I make compose use variadic tuples in the generic signature.


preProcessSnapshot<NewC = ModelCreationType2<PROPS, CustomC>>(
fn: (snapshot: NewC) => WithAdditionalProperties<ModelCreationType2<PROPS, CustomC>>
Expand All @@ -235,6 +244,7 @@ export interface IAnyModelType extends IModelType<any, any, any, any> {}
export type ExtractProps<T extends IAnyModelType> = T extends IModelType<infer P, any, any, any>
? P
: never

/** @hidden */
export type ExtractOthers<T extends IAnyModelType> = T extends IModelType<any, infer O, any, any>
? O
Expand Down Expand Up @@ -458,7 +468,7 @@ export class ModelType<
return this.cloneAndEnhance({ properties })
}

volatile<TP extends object>(fn: (self: Instance<this>) => TP) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing all of these to be clearer. object is generally not what one wants.

CleanShot 2024-11-09 at 11 02 56@2x

I think what we want is only the last.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely agree here.

volatile<TP extends AnyObject>(fn: (self: Instance<this>) => TP) {
if (typeof fn !== "function") {
throw new MstError(
`You passed an ${typeof fn} to volatile state as an argument, when function is expected`
Expand All @@ -483,7 +493,7 @@ export class ModelType<
set(self, state)
}

extend<A extends ModelActions = {}, V extends Object = {}, VS extends Object = {}>(
extend<A extends ModelActions = {}, V extends AnyObject = {}, VS extends AnyObject = {}>(
fn: (self: Instance<this>) => { actions?: A; views?: V; state?: VS }
) {
const initializer = (self: Instance<this>) => {
Expand All @@ -500,15 +510,15 @@ export class ModelType<
return this.cloneAndEnhance({ initializers: [initializer] })
}

views<V extends Object>(fn: (self: Instance<this>) => V) {
views<V extends AnyObject>(fn: (self: Instance<this>) => V) {
const viewInitializer = (self: Instance<this>) => {
this.instantiateViews(self, fn(self))
return self
}
return this.cloneAndEnhance({ initializers: [viewInitializer] })
}

private instantiateViews(self: this["T"], views: Object): void {
private instantiateViews(self: this["T"], views: AnyObject): void {
// check views return
if (!isPlainObject(views))
throw new MstError(`views initializer should return a plain object containing views`)
Expand Down