import { type AbstractModelClass, type ModelClass, modelClass, propsTypeSymbol, } from "../modelShared/BaseModelShared" import type { ModelProps, ModelPropsToTransformedData } from "../modelShared/prop" import type { AnyModel, BaseModelKeys } from "./BaseModel" import { ExtendedModel, Model } from "./Model" import { isModelClass } from "./utils" type ReqObj = [unknown] extends [Req] ? unknown : Req extends object ? Req : never const reqMarkerBrand = Symbol("reqMarkerBrand") /** * Typed requirement marker created by `req()`. * Pass it as the second argument to `defineModelMixin` to declare that the base model must already * contain the `Req` shape before the mixin is applied. */ export type ReqMarker = { readonly [reqMarkerBrand]: Req } const reqMarkerSingleton = { [reqMarkerBrand]: undefined } as unknown as ReqMarker /** * Creates a typed requirement marker for `defineModelMixin`. * * @template Req Shape that must exist on the base model instance. * * @example * const producerMixin = defineModelMixin( * { produced: tProp(types.number, 0) }, * req<{ quantity: number }>() * ) */ export function req(): ReqMarker { return reqMarkerSingleton } function isReqMarker(v: unknown): v is ReqMarker { return typeof v === "object" && v !== null && (reqMarkerBrand as any) in v } /** * Mixin function type for model classes. * * @template Added Shape added by the mixin. * @template Req Shape that must exist in the input model instance. * @template ExtraProps ModelProps to merge into the class's propsTypeSymbol marker. When using * `defineModelMixin`, this is always set to the real `ModelProps` object so that `ModelData` / * `ModelCreationData` resolve to the exact prop types. */ export type ModelMixin< Added extends object, Req = unknown, ExtraProps extends ModelProps = ModelProps, > = >>( base: B ) => ModelClass & Added & { [propsTypeSymbol]: ExtraProps }> type MixinAdded> = M extends ModelMixin ? Added : never type MixinReq> = M extends ModelMixin ? ReqObj : never type MixinExtraProps> = M extends ModelMixin ? P : never type ApplyMixin, M extends ModelMixin> = ModelClass< InstanceType & MixinAdded & { [propsTypeSymbol]: MixinExtraProps } > type ApplyMixins< B extends ModelClass, M extends readonly ModelMixin[], > = M extends readonly [infer First, ...infer Rest] ? First extends ModelMixin ? ApplyMixins, Extract[]>> : never : B /** * Computes the resulting model class after applying a mixin tuple to a base model class. */ export type ComposedModelClass< B extends ModelClass, M extends readonly ModelMixin[], > = ApplyMixins /** * Computes the resulting model instance type after applying a mixin tuple to a base model class. */ export type ComposedModelInstance< B extends ModelClass, M extends readonly ModelMixin[], > = InstanceType> type ValidateMixins< B extends ModelClass, M extends readonly ModelMixin[], > = M extends readonly [infer First, ...infer Rest] ? First extends ModelMixin ? InstanceType extends AnyModel & MixinReq ? readonly [ First, ...ValidateMixins< ApplyMixin, Extract[]> >, ] : never : never : readonly [] /** * Defines a model mixin from a `ModelProps` object. * * `ModelData` / `ModelCreationData` on the composed class resolve to the exact prop types. * * @template MP Model properties type (inferred from `props`). * @param props Model properties object. * * @example * const countableMixin = defineModelMixin({ quantity: tProp(types.number, 0) }) */ export function defineModelMixin( props: MP ): ModelMixin, unknown, MP> /** * Defines a model mixin from a `ModelProps` object, with a requirement on the base model. * * Pass `req()` as the second argument to declare that the base model must already contain the * `Req` shape before the mixin is applied. * * @template Req Shape that must exist on the base model instance (specified via `req()`). * @template MP Model properties type (inferred from `props`). * @param props Model properties object. * @param requirement Requirement marker created by `req()`. * * @example * const producerMixin = defineModelMixin( * { produced: tProp(types.number, 0) }, * req<{ quantity: number }>() * ) */ export function defineModelMixin( props: MP, requirement: ReqMarker ): ModelMixin, Req, MP> /** * Defines a model mixin from a `ModelProps` object and a builder that can add actions and other * methods. * * The props are applied via `ExtendedModel` first; the resulting pre-extended class is passed to * the builder so you can simply `extend Base` without calling `ExtendedModel` yourself. * `ModelData` / `ModelCreationData` resolve to the exact prop types from `MP`. * * @template MP Model properties type (inferred from `props`). * @template C Built class type (inferred from `build` return type). * @param props Model properties object. * @param build Builder receiving the pre-extended base class. * * @example * const countableMixin = defineModelMixin( * { quantity: tProp(types.number, 0) }, * (Base) => class Countable extends Base { * increment() { this.quantity++ } * } * ) */ export function defineModelMixin< MP extends ModelProps, C extends AbstractModelClass>, >( props: MP, build: (base: AbstractModelClass>) => C ): ModelMixin, BaseModelKeys>, unknown, MP> /** * Defines a model mixin from a `ModelProps` object and a builder, with a requirement on the base * model. * * Pass `req()` as the second argument to declare that the base model must already contain the * `Req` shape. The `Base` class received by the builder includes both the prop types and `Req`. * * @template Req Shape that must exist on the base model instance (specified via `req()`). * @template MP Model properties type (inferred from `props`). * @template C Built class type (inferred from `build` return type). * @param props Model properties object. * @param requirement Requirement marker created by `req()`. * @param build Builder receiving the pre-extended base class (typed with both props and `Req`). * * @example * const producerMixin = defineModelMixin( * { produced: tProp(types.number, 0) }, * req<{ quantity: number }>(), * (Base) => class Producer extends Base { * produceTotal() { return this.produced + this.quantity } * } * ) */ export function defineModelMixin< Req extends object, MP extends ModelProps, C extends AbstractModelClass>, >( props: MP, requirement: ReqMarker, build: (base: AbstractModelClass & Req>) => C ): ModelMixin, BaseModelKeys>, Req, MP> // implementation export function defineModelMixin(...args: any[]): any { const props: ModelProps = args[0] let build: ((base: AbstractModelClass) => AbstractModelClass) | undefined if (args.length === 3) { // (props, req, build) build = args[2] } else if (args.length === 2 && !isReqMarker(args[1])) { // (props, build) build = args[1] } // else: (props) or (props, req) — build stays undefined return (>(base: B) => { const extended = ExtendedModel(modelClass(base) as any, props) as AbstractModelClass return (build ? build(extended) : extended) as unknown as ModelClass> }) as ModelMixin } // Empty base used when composeMixins is called without an explicit base class. class MixinEmptyBase extends Model({}) {} /** * Composes several model mixins without an explicit base model class. * * An implicit empty base (`Model({})`) is used. Useful for bundling mixins to reuse across * multiple concrete classes: * * ```ts * const ProductBase = composeMixins(countableMixin, producerMixin) * * @model("myApp/Product") * class Product extends ExtendedModel(ProductBase, {}) {} * ``` * * @template M Mixin tuple type. * @param mixins Mixins to apply. * @returns A model class with all mixin additions applied over an empty base. */ export function composeMixins[]>( ...mixins: M & ValidateMixins ): ComposedModelClass /** * Composes several model mixins over a base model class. * * @template B Base model class. * @template M Mixin tuple type. * @param base Base model class. * @param mixins Mixins to apply. * @returns A model class with all mixin additions applied. */ export function composeMixins< B extends ModelClass, M extends readonly ModelMixin[], >(base: B, ...mixins: M & ValidateMixins): ComposedModelClass // implementation export function composeMixins(...args: any[]): any { let base: ModelClass let mixins: ModelMixin[] if (isModelClass(args[0])) { base = args[0] mixins = args.slice(1) } else { base = MixinEmptyBase mixins = args } let current = base for (const mixin of mixins) { current = mixin(current) } return current }