Skip to content

Commit c2f459b

Browse files
authored
Improve typing for isXYZType narrowing functions (#2190)
1 parent be7c425 commit c2f459b

File tree

14 files changed

+174
-118
lines changed

14 files changed

+174
-118
lines changed

__tests__/core/type-system.test.ts

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,31 @@ import {
1919
ModelPrimitive,
2020
ModelPropertiesDeclaration,
2121
SnapshotOut,
22-
type ISimpleType
22+
type ISimpleType,
23+
isOptionalType,
24+
isUnionType,
25+
type IOptionalIType,
26+
type ITypeUnion,
27+
isMapType,
28+
isArrayType,
29+
isModelType,
30+
isLiteralType,
31+
isPrimitiveType,
32+
isReferenceType,
33+
isIdentifierType,
34+
isRefinementType,
35+
isLateType
2336
} from "../../src"
2437
import { expect, test } from "bun:test"
38+
import type {
39+
DatePrimitive,
40+
IAnyComplexType,
41+
IAnyModelType,
42+
IArrayType,
43+
IMapType,
44+
IReferenceType,
45+
IUnionType
46+
} from "../../src/internal"
2547

2648
type DifferingKeys<ActualT, ExpectedT> = {
2749
[K in keyof ActualT | keyof ExpectedT]: K extends keyof ActualT
@@ -34,22 +56,18 @@ type DifferingKeys<ActualT, ExpectedT> = {
3456
}[keyof ActualT | keyof ExpectedT] &
3557
string
3658

37-
type NotExactErrorMessage<ActualT, ExpectedT> = ActualT extends Record<string, unknown>
38-
? ExpectedT extends Record<string, unknown>
59+
type NotExactErrorMessage<ActualT, ExpectedT> = ActualT extends Record<string, any>
60+
? ExpectedT extends Record<string, any>
3961
? `Mismatched property: ${DifferingKeys<ActualT, ExpectedT>}`
4062
: "Expected a non-object type, but received an object"
41-
: ExpectedT extends Record<string, unknown>
63+
: ExpectedT extends Record<string, any>
4264
? "Expected an object type, but received a non-object type"
4365
: "Types are not exactly equal"
4466

45-
type IsExact<T1, T2> = [T1] extends [T2] ? ([T2] extends [T1] ? Exact<T1, T2> : never) : never
46-
4767
const assertTypesEqual = <ActualT, ExpectedT>(
4868
t: ActualT,
49-
u: IsExact<ActualT, ExpectedT> extends never
50-
? NotExactErrorMessage<ActualT, ExpectedT>
51-
: ExpectedT
52-
): [ActualT, ExpectedT] => [t, u] as [ActualT, ExpectedT]
69+
u: Exact<ActualT, ExpectedT> extends never ? NotExactErrorMessage<ActualT, ExpectedT> : ExpectedT
70+
): [ActualT, ExpectedT] => [t, u] as any
5371

5472
const _: unknown = undefined
5573

@@ -1281,3 +1299,36 @@ test("#2186 substitutability type verification for model types extending a commo
12811299
SubTypeRequired
12821300
)
12831301
})
1302+
1303+
test("#2184 - type narrowing functions should narrow to the expected type", () => {
1304+
const type: unknown = null
1305+
1306+
if (isOptionalType(type)) {
1307+
assertTypesEqual(type, _ as IOptionalIType<IAnyType, [any, ...any[]]>)
1308+
} else if (isUnionType(type)) {
1309+
assertTypesEqual(type, _ as IUnionType<IAnyType[]>)
1310+
} else if (isFrozenType(type)) {
1311+
assertTypesEqual(type, _ as ISimpleType<any>)
1312+
} else if (isMapType(type)) {
1313+
assertTypesEqual(type, _ as IMapType<IAnyType>)
1314+
} else if (isArrayType(type)) {
1315+
assertTypesEqual(type, _ as IArrayType<IAnyType>)
1316+
} else if (isModelType(type)) {
1317+
assertTypesEqual(type, _ as IAnyModelType)
1318+
} else if (isLiteralType(type)) {
1319+
assertTypesEqual(type, _ as ISimpleType<any>)
1320+
} else if (isPrimitiveType(type)) {
1321+
assertTypesEqual(
1322+
type,
1323+
_ as ISimpleType<string> | ISimpleType<number> | ISimpleType<boolean> | typeof DatePrimitive
1324+
)
1325+
} else if (isReferenceType(type)) {
1326+
assertTypesEqual(type, _ as IReferenceType<IAnyComplexType>)
1327+
} else if (isIdentifierType(type)) {
1328+
assertTypesEqual(type, _ as ISimpleType<string> | ISimpleType<number>)
1329+
} else if (isRefinementType(type)) {
1330+
assertTypesEqual(type, _ as IAnyType)
1331+
} else if (isLateType(type)) {
1332+
assertTypesEqual(type, _ as IAnyType)
1333+
}
1334+
})

bun.lockb

0 Bytes
Binary file not shown.

src/types/complex-types/array.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -505,8 +505,6 @@ function areSame(oldNode: AnyNode, newValue: any) {
505505
* @param type
506506
* @returns `true` if the type is an array type.
507507
*/
508-
export function isArrayType<Items extends IAnyType = IAnyType>(
509-
type: IAnyType
510-
): type is IArrayType<Items> {
508+
export function isArrayType(type: unknown): type is IArrayType<IAnyType> {
511509
return isType(type) && (type.flags & TypeFlags.Array) > 0
512510
}

src/types/complex-types/map.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,6 @@ export function map<IT extends IAnyType>(subtype: IT): IMapType<IT> {
509509
* @param type
510510
* @returns `true` if it is a map type.
511511
*/
512-
export function isMapType<Items extends IAnyType = IAnyType>(
513-
type: IAnyType
514-
): type is IMapType<Items> {
512+
export function isMapType(type: unknown): type is IMapType<IAnyType> {
515513
return isType(type) && (type.flags & TypeFlags.Map) > 0
516514
}

src/types/complex-types/model.ts

Lines changed: 89 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,20 @@ export interface ModelPropertiesDeclaration {
8181
*/
8282
export type ModelPropertiesDeclarationToProperties<T extends ModelPropertiesDeclaration> =
8383
T extends { [k: string]: IAnyType } // optimization to reduce nesting
84-
? T
85-
: {
86-
[K in keyof T]: T[K] extends IAnyType // keep IAnyType check on the top to reduce nesting
87-
? T[K]
88-
: T[K] extends string
89-
? IType<string | undefined, string, string>
90-
: T[K] extends number
91-
? IType<number | undefined, number, number>
92-
: T[K] extends boolean
93-
? IType<boolean | undefined, boolean, boolean>
94-
: T[K] extends Date
95-
? IType<number | Date | undefined, number, Date>
96-
: never
97-
}
84+
? T
85+
: {
86+
[K in keyof T]: T[K] extends IAnyType // keep IAnyType check on the top to reduce nesting
87+
? T[K]
88+
: T[K] extends string
89+
? IType<string | undefined, string, string>
90+
: T[K] extends number
91+
? IType<number | undefined, number, number>
92+
: T[K] extends boolean
93+
? IType<boolean | undefined, boolean, boolean>
94+
: T[K] extends Date
95+
? IType<number | Date | undefined, number, Date>
96+
: never
97+
}
9898

9999
/**
100100
* Checks if a value is optional (undefined, any or unknown).
@@ -124,7 +124,9 @@ type IsOptionalValue<C, TV, FV> = undefined extends C ? TV : FV
124124
type DefinablePropsNames<T> = { [K in keyof T]: IsOptionalValue<T[K], never, K> }[keyof T]
125125

126126
/** @hidden */
127-
export type ExtractCFromProps<P extends ModelProperties> = MaybeEmpty<{ [k in keyof P]: P[k]["CreationType"] }>
127+
export type ExtractCFromProps<P extends ModelProperties> = MaybeEmpty<{
128+
[k in keyof P]: P[k]["CreationType"]
129+
}>
128130

129131
/** @hidden */
130132
export type MaybeEmpty<T> = keyof T extends never ? EmptyObject : T
@@ -147,7 +149,7 @@ export type ModelCreationType2<P extends ModelProperties, CustomC> = MaybeEmpty<
147149
keyof P extends never
148150
? _CustomOrOther<CustomC, ModelCreationType<EmptyObject>>
149151
: _CustomOrOther<CustomC, ModelCreationType<ExtractCFromProps<P>>>
150-
>;
152+
>
151153

152154
/** @hidden */
153155
export type ModelSnapshotType<P extends ModelProperties> = {
@@ -227,7 +229,7 @@ export interface IModelType<
227229
/**
228230
* Any model type.
229231
*/
230-
export interface IAnyModelType extends IModelType<any, any, any, any> { }
232+
export interface IAnyModelType extends IModelType<any, any, any, any> {}
231233

232234
/** @hidden */
233235
export type ExtractProps<T extends IAnyModelType> = T extends IModelType<infer P, any, any, any>
@@ -274,78 +276,82 @@ function toPropertiesObject(declaredProps: ModelPropertiesDeclaration): ModelPro
274276
})
275277

276278
// loop through properties and ensures that all items are types
277-
return keysList.reduce((props, key) => {
278-
// warn if user intended a HOOK
279-
if (key in Hook) {
280-
throw new MstError(
281-
`Hook '${key}' was defined as property. Hooks should be defined as part of the actions`
282-
)
283-
}
279+
return keysList.reduce(
280+
(props, key) => {
281+
// warn if user intended a HOOK
282+
if (key in Hook) {
283+
throw new MstError(
284+
`Hook '${key}' was defined as property. Hooks should be defined as part of the actions`
285+
)
286+
}
284287

285-
// the user intended to use a view
286-
const descriptor = Object.getOwnPropertyDescriptor(declaredProps, key)!
287-
if ("get" in descriptor) {
288-
throw new MstError("Getters are not supported as properties. Please use views instead")
289-
}
290-
// undefined and null are not valid
291-
const value = descriptor.value
292-
if (value === null || value === undefined) {
293-
throw new MstError(
294-
"The default value of an attribute cannot be null or undefined as the type cannot be inferred. Did you mean `types.maybe(someType)`?"
295-
)
296-
}
297-
// its a primitive, convert to its type
298-
else if (isPrimitive(value)) {
299-
props[key] = optional(getPrimitiveFactoryFromValue(value), value)
300-
}
301-
// map defaults to empty object automatically for models
302-
else if (value instanceof MapType) {
303-
props[key] = optional(value, {})
304-
} else if (value instanceof ArrayType) {
305-
props[key] = optional(value, [])
306-
}
307-
// its already a type
308-
else if (isType(value)) {
309-
// do nothing, it's already a type
310-
}
311-
// its a function, maybe the user wanted a view?
312-
else if (devMode() && typeof value === "function") {
313-
throw new MstError(
314-
`Invalid type definition for property '${key}', it looks like you passed a function. Did you forget to invoke it, or did you intend to declare a view / action?`
315-
)
316-
}
317-
// no other complex values
318-
else if (devMode() && typeof value === "object") {
319-
throw new MstError(
320-
`Invalid type definition for property '${key}', it looks like you passed an object. Try passing another model type or a types.frozen.`
321-
)
322-
} else {
323-
throw new MstError(
324-
`Invalid type definition for property '${key}', cannot infer a type from a value like '${value}' (${typeof value})`
325-
)
326-
}
288+
// the user intended to use a view
289+
const descriptor = Object.getOwnPropertyDescriptor(declaredProps, key)!
290+
if ("get" in descriptor) {
291+
throw new MstError("Getters are not supported as properties. Please use views instead")
292+
}
293+
// undefined and null are not valid
294+
const value = descriptor.value
295+
if (value === null || value === undefined) {
296+
throw new MstError(
297+
"The default value of an attribute cannot be null or undefined as the type cannot be inferred. Did you mean `types.maybe(someType)`?"
298+
)
299+
}
300+
// its a primitive, convert to its type
301+
else if (isPrimitive(value)) {
302+
props[key] = optional(getPrimitiveFactoryFromValue(value), value)
303+
}
304+
// map defaults to empty object automatically for models
305+
else if (value instanceof MapType) {
306+
props[key] = optional(value, {})
307+
} else if (value instanceof ArrayType) {
308+
props[key] = optional(value, [])
309+
}
310+
// its already a type
311+
else if (isType(value)) {
312+
// do nothing, it's already a type
313+
}
314+
// its a function, maybe the user wanted a view?
315+
else if (devMode() && typeof value === "function") {
316+
throw new MstError(
317+
`Invalid type definition for property '${key}', it looks like you passed a function. Did you forget to invoke it, or did you intend to declare a view / action?`
318+
)
319+
}
320+
// no other complex values
321+
else if (devMode() && typeof value === "object") {
322+
throw new MstError(
323+
`Invalid type definition for property '${key}', it looks like you passed an object. Try passing another model type or a types.frozen.`
324+
)
325+
} else {
326+
throw new MstError(
327+
`Invalid type definition for property '${key}', cannot infer a type from a value like '${value}' (${typeof value})`
328+
)
329+
}
327330

328-
return props
329-
}, { ...declaredProps } as any)
331+
return props
332+
},
333+
{ ...declaredProps } as any
334+
)
330335
}
331336

332337
/**
333338
* @internal
334339
* @hidden
335340
*/
336341
export class ModelType<
337-
PROPS extends ModelProperties,
338-
OTHERS,
339-
CustomC,
340-
CustomS,
341-
MT extends IModelType<PROPS, OTHERS, CustomC, CustomS>
342-
>
342+
PROPS extends ModelProperties,
343+
OTHERS,
344+
CustomC,
345+
CustomS,
346+
MT extends IModelType<PROPS, OTHERS, CustomC, CustomS>
347+
>
343348
extends ComplexType<
344349
ModelCreationType2<PROPS, CustomC>,
345350
ModelSnapshotType2<PROPS, CustomS>,
346351
ModelInstanceType<PROPS, OTHERS>
347352
>
348-
implements IModelType<PROPS, OTHERS, CustomC, CustomS> {
353+
implements IModelType<PROPS, OTHERS, CustomC, CustomS>
354+
{
349355
readonly flags = TypeFlags.Object
350356

351357
/*
@@ -439,8 +445,8 @@ export class ModelType<
439445
const actionInvoker = createActionInvoker(self as any, name, boundAction)
440446
actions[name] = actionInvoker
441447

442-
// See #646, allow models to be mocked
443-
; (!devMode() ? addHiddenFinalProp : addHiddenWritableProp)(self, name, actionInvoker)
448+
// See #646, allow models to be mocked
449+
;(!devMode() ? addHiddenFinalProp : addHiddenWritableProp)(self, name, actionInvoker)
444450
})
445451
}
446452

@@ -515,7 +521,7 @@ export class ModelType<
515521
} else if (typeof descriptor.value === "function") {
516522
// this is a view function, merge as is!
517523
// See #646, allow models to be mocked
518-
; (!devMode() ? addHiddenFinalProp : addHiddenWritableProp)(self, key, descriptor.value)
524+
;(!devMode() ? addHiddenFinalProp : addHiddenWritableProp)(self, key, descriptor.value)
519525
} else {
520526
throw new MstError(`A view member should either be a function or getter based property`)
521527
}
@@ -645,7 +651,7 @@ export class ModelType<
645651
try {
646652
// TODO: FIXME, make sure the observable ref is used!
647653
const atom = getAtom(node.storedValue, name)
648-
; (atom as any).reportObserved()
654+
;(atom as any).reportObserved()
649655
} catch (e) {
650656
throw new MstError(`${name} property is declared twice`)
651657
}
@@ -669,14 +675,14 @@ export class ModelType<
669675
if (!(patch.op === "replace" || patch.op === "add")) {
670676
throw new MstError(`object does not support operation ${patch.op}`)
671677
}
672-
; (node.storedValue as any)[subpath] = patch.value
678+
;(node.storedValue as any)[subpath] = patch.value
673679
}
674680

675681
applySnapshot(node: this["N"], snapshot: this["C"]): void {
676682
typecheckInternal(this, snapshot)
677683
const preProcessedSnapshot = this.applySnapshotPreProcessor(snapshot)
678684
this.forAllProps((name) => {
679-
; (node.storedValue as any)[name] = preProcessedSnapshot[name]
685+
;(node.storedValue as any)[name] = preProcessedSnapshot[name]
680686
})
681687
}
682688

@@ -732,7 +738,7 @@ export class ModelType<
732738
}
733739

734740
removeChild(node: this["N"], subpath: string) {
735-
; (node.storedValue as any)[subpath] = undefined
741+
;(node.storedValue as any)[subpath] = undefined
736742
}
737743
}
738744
ModelType.prototype.applySnapshot = action(ModelType.prototype.applySnapshot)
@@ -853,6 +859,6 @@ export function compose(...args: any[]): any {
853859
* @param type
854860
* @returns
855861
*/
856-
export function isModelType<IT extends IAnyModelType = IAnyModelType>(type: IAnyType): type is IT {
862+
export function isModelType(type: unknown): type is IAnyModelType {
857863
return isType(type) && (type.flags & TypeFlags.Object) > 0
858864
}

src/types/primitives.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,9 @@ export function getPrimitiveFactoryFromValue(value: any): ISimpleType<any> {
238238
* @param type
239239
* @returns
240240
*/
241-
export function isPrimitiveType<
242-
IT extends ISimpleType<string> | ISimpleType<number> | ISimpleType<boolean> | typeof DatePrimitive
243-
>(type: IT): type is IT {
241+
export function isPrimitiveType(
242+
type: unknown
243+
): type is ISimpleType<string> | ISimpleType<number> | ISimpleType<boolean> | typeof DatePrimitive {
244244
return (
245245
isType(type) &&
246246
(type.flags &

0 commit comments

Comments
 (0)