Skip to content

Commit 8238701

Browse files
Improved typing for union types (#2151)
* Have unions not devolve into `IAnyType` with 10+ members. * Improve typings for enums * Remove no longer used scripts/generate-union-types.js * Add an additional literal to union typechecking test * spike: issue 1525 still broken * test: add test for issue 1664 * spike: failing test for 1833 * test: remove failing tests * test: add describe/test blocks * test: remove failing tests (again) * test: add test for issue 1525 --------- Co-authored-by: Tyler Williams <[email protected]>
1 parent 46334b6 commit 8238701

File tree

6 files changed

+141
-181
lines changed

6 files changed

+141
-181
lines changed

__tests__/core/1525.test.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { types, Instance } from "../../src/index"
2+
3+
describe("1525. Model instance maybe fields becoming TypeScript optional fields when included in a types.union", () => {
4+
it("does not throw a typescript error", () => {
5+
const Model = types.model("myModel", {
6+
foo: types.string,
7+
bar: types.maybe(types.integer)
8+
})
9+
10+
const Store = types.model("store", {
11+
itemWithoutIssue: Model,
12+
itemWithIssue: types.union(types.literal("anotherValue"), Model)
13+
})
14+
15+
interface IModel extends Instance<typeof Model> {}
16+
17+
interface FunctionArgs {
18+
model1: IModel
19+
model2: IModel
20+
}
21+
22+
const store = Store.create({
23+
itemWithoutIssue: { foo: "works" },
24+
itemWithIssue: { foo: "has ts error in a regression" }
25+
})
26+
27+
const f = (props: FunctionArgs) => {}
28+
29+
const itemWithoutIssueModel = store.itemWithoutIssue
30+
const itemWithIssueModel = store.itemWithIssue === "anotherValue" ? null : store.itemWithIssue
31+
itemWithIssueModel && f({ model1: itemWithoutIssueModel, model2: itemWithIssueModel })
32+
})
33+
})

__tests__/core/1664.test.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { types as t } from "../../src/index"
2+
3+
describe("1664. Array and model types are not inferred correctly when broken down into their components", () => {
4+
test("should not throw a typescript error", () => {
5+
// Simple concrete type with a creation type different than its instance type
6+
const date = t.custom<string, Date>({
7+
name: "Date",
8+
fromSnapshot: (snapshot) => new Date(snapshot),
9+
toSnapshot: (dt) => dt.toISOString(),
10+
isTargetType: (val: unknown) => val instanceof Date,
11+
getValidationMessage: (snapshot: unknown) =>
12+
typeof snapshot !== "string" || isNaN(Date.parse(snapshot))
13+
? `${snapshot} is not a valid Date string`
14+
: ""
15+
})
16+
17+
//Wrap the date type in an array type. IArrayType is a sub-interface of IType.
18+
const DateArray = t.array(date)
19+
20+
//Pass the array type to t.union, which infers the component types as <C, S, T>
21+
const LoadableDateArray = t.union(t.literal("loading"), DateArray)
22+
23+
//Instantiate the type
24+
const lda = LoadableDateArray.create([])
25+
26+
//Try to use the array type as an instance
27+
if (lda !== "loading") {
28+
//Error: type of lda is essentially `(string | Date)[] | undefined`
29+
//The creation type has been mixed together with the instance type
30+
const dateArray: Date[] = lda
31+
}
32+
})
33+
})

__tests__/core/type-system.test.ts

+43-5
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,21 @@ type DifferingKeys<ActualT, ExpectedT> = {
3232
}[keyof ActualT | keyof ExpectedT] &
3333
string
3434

35-
type InexactErrorMessage<ActualT, ExpectedT> = `Mismatched property: ${DifferingKeys<
36-
ActualT,
37-
ExpectedT
38-
>}`
35+
type NotExactErrorMessage<ActualT, ExpectedT> = ActualT extends Record<string, unknown>
36+
? ExpectedT extends Record<string, unknown>
37+
? `Mismatched property: ${DifferingKeys<ActualT, ExpectedT>}`
38+
: "Expected a non-object type, but received an object"
39+
: ExpectedT extends Record<string, unknown>
40+
? "Expected an object type, but received a non-object type"
41+
: "Types are not exactly equal"
42+
43+
type IsExact<T1, T2> = [T1] extends [T2] ? ([T2] extends [T1] ? Exact<T1, T2> : never) : never
3944

4045
const assertTypesEqual = <ActualT, ExpectedT>(
4146
t: ActualT,
42-
u: Exact<ActualT, ExpectedT> extends never ? InexactErrorMessage<ActualT, ExpectedT> : ExpectedT
47+
u: IsExact<ActualT, ExpectedT> extends never
48+
? NotExactErrorMessage<ActualT, ExpectedT>
49+
: ExpectedT
4350
): [ActualT, ExpectedT] => [t, u] as [ActualT, ExpectedT]
4451
const _: unknown = undefined
4552

@@ -1225,3 +1232,34 @@ test("object creation when composing with a model with no props", () => {
12251232
// @ts-expect-error -- unknown prop
12261233
true || Composed.create({ another: 5 })
12271234
})
1235+
1236+
test("union type inference verification for small number of types", () => {
1237+
const T = types.union(types.boolean, types.literal("test"), types.maybe(types.number))
1238+
1239+
type ITC = SnapshotIn<typeof T>
1240+
type ITS = SnapshotOut<typeof T>
1241+
1242+
assertTypesEqual(_ as ITC, _ as boolean | "test" | number | undefined)
1243+
assertTypesEqual(_ as ITS, _ as boolean | "test" | number | undefined)
1244+
})
1245+
1246+
test("union type inference verification for a large number of types", () => {
1247+
const T = types.union(
1248+
types.literal("a"),
1249+
types.literal("b"),
1250+
types.literal("c"),
1251+
types.literal("d"),
1252+
types.literal("e"),
1253+
types.literal("f"),
1254+
types.literal("g"),
1255+
types.literal("h"),
1256+
types.literal("i"),
1257+
types.literal("j")
1258+
)
1259+
1260+
type ITC = SnapshotIn<typeof T>
1261+
type ITS = SnapshotOut<typeof T>
1262+
1263+
assertTypesEqual(_ as ITC, _ as "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j")
1264+
assertTypesEqual(_ as ITS, _ as "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j")
1265+
})

scripts/generate-union-types.js

-69
This file was deleted.

src/types/utility-types/enumeration.ts

+10-8
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@ export type UnionStringArray<T extends readonly string[]> = T[number]
88
// these overloads also allow both mutable and immutable arrays, making types.enumeration<Enum>(Object.values(Enum)) possible.
99
// the only case where this doesn't work is when passing to the function an array variable with a mutable type constraint;
1010
// for these cases, it will just fallback and assume the type is a generic string.
11-
export function enumeration<T extends readonly string[]>(
12-
options: T
13-
): ISimpleType<UnionStringArray<T>>
11+
export function enumeration<T extends string>(
12+
options: readonly T[]
13+
): ISimpleType<UnionStringArray<T[]>>
1414
export function enumeration<T extends string>(
1515
name: string,
16-
options: T[]
16+
options: readonly T[]
1717
): ISimpleType<UnionStringArray<T[]>>
18-
1918
/**
2019
* `types.enumeration` - Can be used to create an string based enumeration.
2120
* (note: this methods is just sugar for a union of string literals)
@@ -31,8 +30,11 @@ export function enumeration<T extends string>(
3130
* @param options possible values this enumeration can have
3231
* @returns
3332
*/
34-
export function enumeration(name: string | string[], options?: any): ISimpleType<string> {
35-
const realOptions: string[] = typeof name === "string" ? options! : name
33+
export function enumeration<T extends string>(
34+
name: string | readonly T[],
35+
options?: readonly T[]
36+
): ISimpleType<T[number]> {
37+
const realOptions: readonly T[] = typeof name === "string" ? options! : name
3638
// check all options
3739
if (devMode()) {
3840
realOptions.forEach((option, i) => {
@@ -41,5 +43,5 @@ export function enumeration(name: string | string[], options?: any): ISimpleType
4143
}
4244
const type = union(...realOptions.map((option) => literal("" + option)))
4345
if (typeof name === "string") type.name = name
44-
return type
46+
return type as ISimpleType<T[number]>
4547
}

0 commit comments

Comments
 (0)