ianstormtaylor / superstruct

A simple and composable way to validate data in JavaScript (and TypeScript).
https://docs.superstructjs.org
MIT License
6.96k stars 223 forks source link

`intersection` flattens unions too much #1180

Open Gelio opened 1 year ago

Gelio commented 1 year ago

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.

morlay commented 1 year ago
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.