superstruct icon indicating copy to clipboard operation
superstruct copied to clipboard

`intersection` flattens unions too much

Open Gelio opened this issue 2 years ago • 1 comments

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

TypeScript playground

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.

Gelio avatar Apr 28 '23 12:04 Gelio

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.

morlay avatar May 12 '23 11:05 morlay