superstruct
superstruct copied to clipboard
`intersection` flattens unions too much
Because intersection uses UnionToIntersection on its arguments (except for the first one), it flattens any unions that appear in the intersection.
import * as s from "superstruct";
import { expectType, type TypeEqual } from "ts-expect";
const baseUnion = s.union([
s.type({
flavor: s.literal("a"),
}),
s.type({
flavor: s.literal("b"),
extraProperty: s.string(),
}),
]);
const someOtherObject = s.type({
foo: s.literal("bar")
});
const unionAsFirstInIntersection = s.intersection([
baseUnion,
someOtherObject,
]);
type ExpectedResult = ({ flavor: "a" } | { flavor: "b"; extraProperty: string }) & { foo: "bar" };
expectType<TypeEqual<s.Infer<typeof unionAsFirstInIntersection>, ExpectedResult>>(true);
const unionAsSecondInIntersection = s.intersection([
someOtherObject,
baseUnion,
]);
expectType<TypeEqual<s.Infer<typeof unionAsSecondInIntersection>, ExpectedResult>>(true); // error
expectType<TypeEqual<typeof unionAsSecondInIntersection, s.Struct<never, null>>>(false); // error
Notice that when baseUnion is used as the first in the intersection array, it works fine. When baseUnion appears second, UnionToIntesection flattens it too much, which causes the final unionAsSecondInIntersection struct to be never.
Workaround
Use union as the first element of the array provided to intersection. unionAsFirstInIntersection is constructed correctly.
type IntersectionTypes<Types extends any[]> = Types extends [
infer T,
...infer O
]
? T extends Struct<any, any>
? Infer<T> & IntersectionTypes<O>
: unknown
: unknown;
export function intersection<Types extends [...StructAny[]]>(
...types: Types
): Type<IntersectionTypes<Types>, null> {
return ss.intersection(types as any) as any
}
I fixed with this.