diff --git a/__tests__/core/1525.test.ts b/__tests__/core/1525.test.ts new file mode 100644 index 000000000..b073fbb52 --- /dev/null +++ b/__tests__/core/1525.test.ts @@ -0,0 +1,33 @@ +import { types, Instance } from "../../src/index" + +describe("1525. Model instance maybe fields becoming TypeScript optional fields when included in a types.union", () => { + it("does not throw a typescript error", () => { + const Model = types.model("myModel", { + foo: types.string, + bar: types.maybe(types.integer) + }) + + const Store = types.model("store", { + itemWithoutIssue: Model, + itemWithIssue: types.union(types.literal("anotherValue"), Model) + }) + + interface IModel extends Instance {} + + interface FunctionArgs { + model1: IModel + model2: IModel + } + + const store = Store.create({ + itemWithoutIssue: { foo: "works" }, + itemWithIssue: { foo: "has ts error in a regression" } + }) + + const f = (props: FunctionArgs) => {} + + const itemWithoutIssueModel = store.itemWithoutIssue + const itemWithIssueModel = store.itemWithIssue === "anotherValue" ? null : store.itemWithIssue + itemWithIssueModel && f({ model1: itemWithoutIssueModel, model2: itemWithIssueModel }) + }) +}) diff --git a/__tests__/core/1664.test.ts b/__tests__/core/1664.test.ts new file mode 100644 index 000000000..ff31a61c3 --- /dev/null +++ b/__tests__/core/1664.test.ts @@ -0,0 +1,33 @@ +import { types as t } from "../../src/index" + +describe("1664. Array and model types are not inferred correctly when broken down into their components", () => { + test("should not throw a typescript error", () => { + // Simple concrete type with a creation type different than its instance type + const date = t.custom({ + name: "Date", + fromSnapshot: (snapshot) => new Date(snapshot), + toSnapshot: (dt) => dt.toISOString(), + isTargetType: (val: unknown) => val instanceof Date, + getValidationMessage: (snapshot: unknown) => + typeof snapshot !== "string" || isNaN(Date.parse(snapshot)) + ? `${snapshot} is not a valid Date string` + : "" + }) + + //Wrap the date type in an array type. IArrayType is a sub-interface of IType. + const DateArray = t.array(date) + + //Pass the array type to t.union, which infers the component types as + const LoadableDateArray = t.union(t.literal("loading"), DateArray) + + //Instantiate the type + const lda = LoadableDateArray.create([]) + + //Try to use the array type as an instance + if (lda !== "loading") { + //Error: type of lda is essentially `(string | Date)[] | undefined` + //The creation type has been mixed together with the instance type + const dateArray: Date[] = lda + } + }) +}) diff --git a/__tests__/core/type-system.test.ts b/__tests__/core/type-system.test.ts index 953989e7e..8771e1aaf 100644 --- a/__tests__/core/type-system.test.ts +++ b/__tests__/core/type-system.test.ts @@ -32,14 +32,21 @@ type DifferingKeys = { }[keyof ActualT | keyof ExpectedT] & string -type InexactErrorMessage = `Mismatched property: ${DifferingKeys< - ActualT, - ExpectedT ->}` +type NotExactErrorMessage = ActualT extends Record + ? ExpectedT extends Record + ? `Mismatched property: ${DifferingKeys}` + : "Expected a non-object type, but received an object" + : ExpectedT extends Record + ? "Expected an object type, but received a non-object type" + : "Types are not exactly equal" + +type IsExact = [T1] extends [T2] ? ([T2] extends [T1] ? Exact : never) : never const assertTypesEqual = ( t: ActualT, - u: Exact extends never ? InexactErrorMessage : ExpectedT + u: IsExact extends never + ? NotExactErrorMessage + : ExpectedT ): [ActualT, ExpectedT] => [t, u] as [ActualT, ExpectedT] const _: unknown = undefined @@ -1225,3 +1232,34 @@ test("object creation when composing with a model with no props", () => { // @ts-expect-error -- unknown prop true || Composed.create({ another: 5 }) }) + +test("union type inference verification for small number of types", () => { + const T = types.union(types.boolean, types.literal("test"), types.maybe(types.number)) + + type ITC = SnapshotIn + type ITS = SnapshotOut + + assertTypesEqual(_ as ITC, _ as boolean | "test" | number | undefined) + assertTypesEqual(_ as ITS, _ as boolean | "test" | number | undefined) +}) + +test("union type inference verification for a large number of types", () => { + const T = types.union( + types.literal("a"), + types.literal("b"), + types.literal("c"), + types.literal("d"), + types.literal("e"), + types.literal("f"), + types.literal("g"), + types.literal("h"), + types.literal("i"), + types.literal("j") + ) + + type ITC = SnapshotIn + type ITS = SnapshotOut + + assertTypesEqual(_ as ITC, _ as "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j") + assertTypesEqual(_ as ITS, _ as "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j") +}) diff --git a/scripts/generate-union-types.js b/scripts/generate-union-types.js deleted file mode 100644 index 39313ce92..000000000 --- a/scripts/generate-union-types.js +++ /dev/null @@ -1,69 +0,0 @@ -const { getDeclaration } = require("./generate-shared") - -let str = `// generated with ${__filename}\n` - -const minArgs = 2 -const maxArgs = 10 -const preParam = "options: UnionOptions, " -const modelReturnTypeTransform = (rt) => { - // [['PA', 'PB'], ['OA', 'OB'], ['FCA', 'FCB'], ['FSA', 'FSB']] - // -> - // [['ModelCreationType2', 'ModelCreationType2'], - // ['ModelSnapshotType2', 'ModelSnapshotType2'], - // ['ModelInstanceType', 'ModelInstanceType']] - const [props, others, fixedC, fixedS] = rt - - const c = [], - s = [], - t = [] - for (let i = 0; i < props.length; i++) { - const p = props[i] - const o = others[i] - const fc = fixedC[i] - const fs = fixedS[i] - - c.push(`ModelCreationType2<${p}, ${fc}>`) - s.push(`ModelSnapshotType2<${p}, ${fs}>`) - t.push(`ModelInstanceType<${p}, ${o}>`) - } - return [c, s, t] -} - -for (let i = minArgs; i < maxArgs; i++) { - str += getDeclaration( - "union", - "IModelType", - ["P", "O", "FC", "FS"], - i, - null, - "|", - "ITypeUnion", - modelReturnTypeTransform - ) - str += getDeclaration( - "union", - "IModelType", - ["P", "O", "FC", "FS"], - i, - preParam, - "|", - "ITypeUnion", - modelReturnTypeTransform - ) -} - -for (let i = minArgs; i < maxArgs; i++) { - str += getDeclaration("union", "IType", ["C", "S", "T"], i, null, "|", "ITypeUnion", undefined) - str += getDeclaration( - "union", - "IType", - ["C", "S", "T"], - i, - preParam, - "|", - "ITypeUnion", - undefined - ) -} - -console.log(str) diff --git a/src/types/utility-types/enumeration.ts b/src/types/utility-types/enumeration.ts index 44beb3f4e..48cc86bcc 100644 --- a/src/types/utility-types/enumeration.ts +++ b/src/types/utility-types/enumeration.ts @@ -8,14 +8,13 @@ export type UnionStringArray = T[number] // these overloads also allow both mutable and immutable arrays, making types.enumeration(Object.values(Enum)) possible. // the only case where this doesn't work is when passing to the function an array variable with a mutable type constraint; // for these cases, it will just fallback and assume the type is a generic string. -export function enumeration( - options: T -): ISimpleType> +export function enumeration( + options: readonly T[] +): ISimpleType> export function enumeration( name: string, - options: T[] + options: readonly T[] ): ISimpleType> - /** * `types.enumeration` - Can be used to create an string based enumeration. * (note: this methods is just sugar for a union of string literals) @@ -31,8 +30,11 @@ export function enumeration( * @param options possible values this enumeration can have * @returns */ -export function enumeration(name: string | string[], options?: any): ISimpleType { - const realOptions: string[] = typeof name === "string" ? options! : name +export function enumeration( + name: string | readonly T[], + options?: readonly T[] +): ISimpleType { + const realOptions: readonly T[] = typeof name === "string" ? options! : name // check all options if (devMode()) { realOptions.forEach((option, i) => { @@ -41,5 +43,5 @@ export function enumeration(name: string | string[], options?: any): ISimpleType } const type = union(...realOptions.map((option) => literal("" + option))) if (typeof name === "string") type.name = name - return type + return type as ISimpleType } diff --git a/src/types/utility-types/union.ts b/src/types/utility-types/union.ts index 60ac01a87..0d425f6cf 100644 --- a/src/types/utility-types/union.ts +++ b/src/types/utility-types/union.ts @@ -11,11 +11,6 @@ import { isPlainObject, IAnyType, IValidationError, - IModelType, - ModelProperties, - ModelInstanceType, - ModelSnapshotType2, - ModelCreationType2, _NotCustomized, AnyObjectNode, BaseType, @@ -35,7 +30,11 @@ export interface UnionOptions { * @internal * @hidden */ -export class Union extends BaseType { +export class Union extends BaseType< + _CustomCSProcessor, + _CustomCSProcessor, + Types[number]["TypeWithoutSTN"] +> { private readonly _dispatcher?: ITypeDispatcher private readonly _eager: boolean = true @@ -49,7 +48,7 @@ export class Union extends BaseType { return result } - constructor(name: string, private readonly _types: IAnyType[], options?: UnionOptions) { + constructor(name: string, private readonly _types: Types, options?: UnionOptions) { super(name) options = { eager: true, @@ -99,7 +98,7 @@ export class Union extends BaseType { return this._dispatcher(value) } - // find the most accomodating type + // find the most accommodating type // if we are using reconciliation try the current node type first (fix for #1045) if (reconcileCurrentType) { if (reconcileCurrentType.is(value)) { @@ -152,96 +151,17 @@ export type _CustomCSProcessor = Exclude extends never export interface ITypeUnion extends IType<_CustomCSProcessor, _CustomCSProcessor, T> {} -// generated with packages/mobx-state-tree/scripts/generate-union-types.js -// prettier-ignore -export function union(A: IModelType, B: IModelType): ITypeUnion | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(options: UnionOptions, A: IModelType, B: IModelType): ITypeUnion | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(A: IModelType, B: IModelType, C: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(options: UnionOptions, A: IModelType, B: IModelType, C: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(A: IModelType, B: IModelType, C: IModelType, D: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(options: UnionOptions, A: IModelType, B: IModelType, C: IModelType, D: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(options: UnionOptions, A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(options: UnionOptions, A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(options: UnionOptions, A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: - IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType, H: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(options: UnionOptions, A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType, H: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType, H: IModelType, I: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(options: UnionOptions, A: IModelType, B: IModelType, C: IModelType, D: IModelType, E: IModelType, F: IModelType, G: IModelType, H: IModelType, I: IModelType): ITypeUnion | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2 | ModelCreationType2, ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2 | ModelSnapshotType2, ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType | ModelInstanceType> -// prettier-ignore -export function union(A: IType, B: IType): ITypeUnion -// prettier-ignore -export function union(options: UnionOptions, A: IType, B: IType): ITypeUnion -// prettier-ignore -export function union(A: IType, B: IType, C: IType): ITypeUnion -// prettier-ignore -export function union(options: UnionOptions, A: IType, B: IType, C: IType): ITypeUnion -// prettier-ignore -export function union(A: IType, B: IType, C: IType, D: IType): ITypeUnion -// prettier-ignore -export function union(options: UnionOptions, A: IType, B: IType, C: IType, D: IType): ITypeUnion -// prettier-ignore -export function union(A: IType, B: IType, C: IType, D: IType, E: IType): ITypeUnion -// prettier-ignore -export function union(options: UnionOptions, A: IType, B: IType, C: IType, D: IType, E: IType): ITypeUnion -// prettier-ignore -export function union(A: IType, B: IType, C: IType, D: IType, E: IType, F: IType): ITypeUnion -// prettier-ignore -export function union(options: UnionOptions, A: IType, B: IType, C: IType, D: IType, E: IType, F: IType): ITypeUnion -// prettier-ignore -export function union(A: IType, B: IType, C: IType, D: IType, E: IType, F: IType, G: IType): ITypeUnion -// prettier-ignore -export function union(options: UnionOptions, A: IType, B: IType, C: IType, D: IType, E: IType, F: IType, G: IType): ITypeUnion -// prettier-ignore -export function union(A: IType, B: IType, C: IType, D: IType, E: IType, F: IType, G: IType, H: IType): ITypeUnion -// prettier-ignore -export function union(options: UnionOptions, A: IType, B: IType, C: IType, D: IType, E: IType, F: IType, G: IType, H: IType): ITypeUnion -// prettier-ignore -export function union(A: IType, B: IType, C: IType, D: IType, E: IType, F: IType, G: IType, H: IType, I: IType): ITypeUnion -// prettier-ignore -export function union(options: UnionOptions, A: IType, B: IType, C: IType, D: IType, E: IType, F: IType, G: IType, H: IType, I: IType): ITypeUnion - -// manually written -export function union(...types: IAnyType[]): IAnyType -export function union(dispatchOrType: UnionOptions | IAnyType, ...otherTypes: IAnyType[]): IAnyType +type IUnionType = ITypeUnion< + Types[number]["CreationType"], + Types[number]["SnapshotType"], + Types[number]["TypeWithoutSTN"] +> + +export function union(...types: Types): IUnionType +export function union( + options: UnionOptions, + ...types: Types +): IUnionType /** * `types.union` - Create a union of multiple types. If the correct type cannot be inferred unambiguously from a snapshot, provide a dispatcher function of the form `(snapshot) => Type`. * @@ -249,7 +169,10 @@ export function union(dispatchOrType: UnionOptions | IAnyType, ...otherTypes: IA * @param otherTypes * @returns */ -export function union(optionsOrType: UnionOptions | IAnyType, ...otherTypes: IAnyType[]): IAnyType { +export function union( + optionsOrType: UnionOptions | Types[number], + ...otherTypes: Types +): IUnionType { const options = isType(optionsOrType) ? undefined : optionsOrType const types = isType(optionsOrType) ? [optionsOrType, ...otherTypes] : otherTypes const name = "(" + types.map((type) => type.name).join(" | ") + ")"