Skip to content

Schema.fromBranded loses type information about the union brands #8538

Description

@nikelborm

What version of Effect is running?

4.0.0-rc.117

What steps can reproduce the bug?

All of the annotated code is available in the repo v3-v4-effect-repros:

git clone -b main https://github.com/nikelborm/v3-v4-effect-repros.git
cd v3-v4-effect-repros/effect-v3-behavior/
bun i
# All of the errors are expected
bun tsc
cd ../effect-v4-behavior/
bun i
# 1 expected error is missing, and 1 other unexpected error is present
bun tsc
import { Brand, Schema } from "effect";

type MIDIPortType = "input" | "output";

type Id<TPortType extends MIDIPortType> =
	// for distribution
	TPortType extends MIDIPortType
		? Brand.Branded<string, "MIDIPortId"> & Brand.Brand<TPortType>
		: never;

// | (string & Brand.Brand<"MIDIPortId"> & Brand.Brand<"input">)
// | (string & Brand.Brand<"MIDIPortId"> & Brand.Brand<"output">)
export type InputOrOutputMidiPortId = Id<MIDIPortType>;

// Brand.Constructor<InputOrOutputMidiPortId>
export const InputOrOutputMidiPortId = Brand.check<InputOrOutputMidiPortId>(
	Schema.isTrimmed(),
	Schema.isMinLength(5),
);

// UNEXPECTED: no 'input' | 'output' anywhere:
// Schema.brand<Schema.String, "MIDIPortId">
const InputOrOutputMidiPortIdBrokenSchema = Schema.String.pipe(
	Schema.fromBrand("InputOrOutputMidiPortId", InputOrOutputMidiPortId),
);

What is the expected behavior?

The same behavior that I otherwise need to achieve with this workaround:

const Base = Schema.String.pipe(
	Schema.fromBrand("MIDIPortId", InputOrOutputMidiPortId),
);

// Deliberately not using Schema.toType(Base) to avoid running the checks twice
const BaseType = Schema.brand("MIDIPortId")(Schema.Unknown as unknown as Schema.String)

return Schema.Union([
	BaseType.pipe(Schema.brand("output")),
	BaseType.pipe(Schema.brand("input")),
]).pipe(Schema.encodeTo(Base));

What do you see instead?

Essentially, these 2 produce different types:

Option.getOrThrow(InputOrOutputMidiPortIdBrand.option('12345'))

// loses 'input' | 'output'
Schema.decodeSync(InputOrOutputMidiPortIdSchema)('12325')
index.ts:59:38 - error TS1360: Type 'string & Brand<"MIDIPortId">' does not satisfy the expected type 'string & ((Brand<"MIDIPortId"> & Brand<"input">) | (Brand<"MIDIPortId"> & Brand<"output">))'.
  Type 'string & Brand<"MIDIPortId">' is not assignable to type 'string & Brand<"MIDIPortId"> & Brand<"output">'.
    Type 'string & Brand<"MIDIPortId">' is not assignable to type 'Brand<"output">'.
      Types of property '[TypeId]' are incompatible.
        Property 'output' is missing in type '{ readonly MIDIPortId: "MIDIPortId"; }' but required in type '{ readonly output: "output"; }'.

59 Schema.decodeSync(IdSchema)("12325") satisfies string &
                                        ~~~~~~~~~

Additional information

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions