Effect-TS / schema

Modeling the schema of data structures as first-class values

Home Page:https://effect.website

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Discrimination Union in @effect/schema Library

ninesunsabiu opened this issue · comments

Hello, thank you for this interesting repository.

Recently, I have been trying to use the @effect/* libraries and I have a question regarding discrimination union.

I am currently migrating from io-ts/Decoder to @effect/schema and I noticed that in io-ts/Decoder, I can use sum(tag)(...members) to handle tagged unions, but in @effect/schema, only the union combinator is available.

io-ts

image

@effect/schema

image

For tagged unions, only the tag matched will run decode, which is more efficient in terms of performance. It seems that union does not have this effect.

Would there be a combinator similar to sum available in @effect/schema?

In effect/schema, unions are consistently defined with the union combinator, similar to TypeScript, without any special APIs for tagged unions.

The library automatically identifies if a union is tagged and optimizes the decoding process accordingly

Thank you for your response. It seems that when writing a tagged union, as long as it is defined in a similar way to TypeScript types, it will be automatically optimized.

Initially, I followed the instructions in the README.md-Discriminated Unions and used S.attachPropertySignature to define the tagged union.

const ResData = S.union(
	F.pipe(
		S.struct({ data: S.struct({ id: S.number, name: S.string }) }),
		S.attachPropertySignature("code", 0)
	),
	F.pipe(S.struct({ msg: S.string }), S.attachPropertySignature("code", -1))
)

const data = {
	data: { id: 1 },
	code: 0,
}

const parsedResult = S.parseEither(ResData)(data)

if (parsedResult._tag === "Left") {
	console.log("data: ", JSON.stringify(data))
	console.error("parse error:", "\n", formatErrors(parsedResult.left.errors))
}
/*

it output
Request.ts:45 parse error: 
 error(s) found
├─ union member
│  └─ ["data"]["name"]
│     └─ is missing
└─ union member
   └─ ["msg"]
      └─ is missing

*/
}

However, based on the results, it seems that this method did not achieve optimization. But when I tried to define the tagged union in the same way as in TypeScript, it was optimized! This is amazing.

const ResData = S.union(
	F.pipe(
		S.struct({
			data: S.struct({ id: S.number, name: S.string }),
			code: S.literal(0),
		})
	),
	F.pipe(S.struct({ msg: S.string, code: S.literal(-1) }))
)

// ... 

/*
react_devtools_backend_compact.js:2367 parse error: 
 error(s) found
└─ union member
   └─ ["data"]["name"]
      └─ is missing
*/

If you don't mind, could you explain how performance optimization is handled in the case "Union" of the Parser.ts interpreter? I'm not quite sure I understand it.

attachPropertySignature doesn't define a tagged union, it transforms an untagged union into a tagged union (so the optimization doesn't apply):

/*
const ResData: S.Schema<{
    readonly data: {
        readonly name: string;
        readonly id: number;
    };
} | {
    readonly msg: string;
}, {
    readonly data: {
        readonly name: string;
        readonly id: number;
    };
    readonly code: 0;
} | {
    readonly msg: string;
    readonly code: -1;
}>
*/
const ResData = S.union(
  pipe(
    S.struct({ data: S.struct({ id: S.number, name: S.string }) }),
    S.attachPropertySignature('code', 0)
  ),
  pipe(S.struct({ msg: S.string }), S.attachPropertySignature('code', -1))
)

const data = {
  data: { id: 1 },
  code: 0 // <= this is ignored
}

S.parse(ResData)(data)
/*
throws
Error: error(s) found
├─ union member
│  └─ ["data"]["name"]
│     └─ is missing
└─ union member
   └─ ["msg"]
      └─ is missing
*/

you get the same output with

const data2 = {
  data: { id: 1 }
}

S.parse(ResData)(data2)
/*
throws
Error: error(s) found
├─ union member
│  └─ ["data"]["name"]
│     └─ is missing
└─ union member
   └─ ["msg"]
      └─ is missing
*/

i.e. the input is still an untagged union

{
    readonly data: {
        readonly name: string;
        readonly id: number;
    };
} | {
    readonly msg: string;
}

but the output will be a tagged union

{
    readonly data: {
        readonly name: string;
        readonly id: number;
    };
    readonly code: 0;
} | {
    readonly msg: string;
    readonly code: -1;
}

this is useful when you don't control the input format and you receive an untagged union but you still want a tagged union in your domain model.

In your second definition instead you are actually defining a tagged union in the first place, so /schema can apply an optimization.

Thank you very much for your patient reply.