diff --git a/.changeset/add-prefer-schema-union.md b/.changeset/add-prefer-schema-union.md
new file mode 100644
index 00000000..d06af760
--- /dev/null
+++ b/.changeset/add-prefer-schema-union.md
@@ -0,0 +1,22 @@
+---
+"@effect/tsgo": minor
+---
+
+Add `preferSchemaUnion` diagnostic (`TS377130`) and code fix for Effect v4 to suggest composing Schema values with `Schema.Union` instead of unioning their extracted `Type` properties.
+
+### Example
+
+```ts
+// Before
+export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+
+export type Shape = typeof Circle.Type | typeof Square.Type
+
+// After applying code fix
+export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+
+export const Shape = Schema.Union([Circle, Square])
+export type Shape = typeof Shape.Type
+```
diff --git a/README.md b/README.md
index b3778c5c..95100022 100644
--- a/README.md
+++ b/README.md
@@ -151,6 +151,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu
newSchemaClass | Suggests using Schema make instead of new for Schema classes |
optionMatchToFromOption | Suggests Effect.fromOption when Option.match or an Option tag conditional only converts Some to Effect.succeed and None to Effect.fail |
preferSchemaTypeProperty | Disallows Schema.Schema.Type<typeof X> in favor of typeof X.Type |
+ preferSchemaUnion | Suggests composing Schema values with Schema.Union instead of unioning their extracted Type properties |
preferSucceedSomeOrNone | Suggests using Effect.succeedNone or Effect.succeedSome instead of wrapping Option.none or Option.some with Effect.succeed |
preferTypedSchemaDecoder | Suggests typed Schema decoders when the input is assignable to the schema's Encoded type |
provideLayerSucceedToProvideService | Suggests providing inline Layer.succeed and Layer.effect services directly |
diff --git a/_packages/tsgo/src/metadata.json b/_packages/tsgo/src/metadata.json
index b4483bac..7850afc3 100644
--- a/_packages/tsgo/src/metadata.json
+++ b/_packages/tsgo/src/metadata.json
@@ -2232,6 +2232,29 @@
]
}
},
+ {
+ "name": "preferSchemaUnion",
+ "group": "style",
+ "description": "Suggests composing Schema values with Schema.Union instead of unioning their extracted Type properties",
+ "defaultSeverity": "off",
+ "fixable": true,
+ "supportedEffect": [
+ "v4"
+ ],
+ "codes": [
+ 377130
+ ],
+ "preview": {
+ "sourceText": "import { Schema } from \"effect\"\n\nexport const Circle = Schema.Struct({ kind: Schema.Literal(\"circle\"), radius: Schema.Number })\nexport const Square = Schema.Struct({ kind: Schema.Literal(\"square\"), side: Schema.Number })\n\nexport type Shape = typeof Circle.Type | typeof Square.Type\n",
+ "diagnostics": [
+ {
+ "start": 242,
+ "end": 281,
+ "text": "This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)"
+ }
+ ]
+ }
+ },
{
"name": "preferSucceedSomeOrNone",
"group": "style",
diff --git a/docs/rules/prefer-schema-union.md b/docs/rules/prefer-schema-union.md
new file mode 100644
index 00000000..fab1e815
--- /dev/null
+++ b/docs/rules/prefer-schema-union.md
@@ -0,0 +1,66 @@
+
+
+# `preferSchemaUnion`
+
+Suggests composing Schema values with Schema.Union instead of unioning their extracted Type properties
+
+| Property | Value |
+| --- | --- |
+| Category | Style |
+| Default severity | `off` |
+| Fixable | Yes |
+| Effect versions | v4 |
+| Diagnostic codes | `TS377130` |
+| Language Service name | `preferSchemaUnion` |
+| Oxlint name | `effecttsgo/prefer-schema-union` |
+
+## Preview
+
+```ts
+import { Schema } from "effect"
+
+export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+
+export type Shape = typeof Circle.Type | typeof Square.Type
+/**
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ effecttsgo(prefer-schema-union): This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema.
+*/
+```
+
+## Language Service Configuration
+
+See the [Language Service setup guide](../../README.md#installation) for installation instructions.
+
+```jsonc
+{
+ "$schema": "./node_modules/@effect/tsgo/schema.json",
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "@effect/language-service",
+ "diagnosticSeverity": {
+ "preferSchemaUnion": "warning"
+ }
+ }
+ ]
+ }
+}
+```
+
+## Oxlint Configuration
+
+See the [Oxlint setup guide](../README.md#oxlint-setup) for installation and patching instructions.
+
+```json
+{
+ "$schema": "./node_modules/@effect/tsgo/oxlint-schema.json",
+ "options": {
+ "typeAware": true
+ },
+ "plugins": ["effecttsgo"],
+ "rules": {
+ "effecttsgo/prefer-schema-union": "warn"
+ }
+}
+```
diff --git a/internal/diagnostics/effectDiagnosticMessages.json b/internal/diagnostics/effectDiagnosticMessages.json
index 73c2c660..7ba4535e 100644
--- a/internal/diagnostics/effectDiagnosticMessages.json
+++ b/internal/diagnostics/effectDiagnosticMessages.json
@@ -506,5 +506,9 @@
"This module reference imports `{0}`, which is obsolete in Effect v4. In Effect v4, Schema is provided directly by `Schema` from `effect` (or `effect/Schema`). effect(obsoleteSchemaImport)": {
"category": "Warning",
"code": 377128
+ },
+ "This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)": {
+ "category": "Suggestion",
+ "code": 377130
}
}
diff --git a/internal/fixables/fixables.go b/internal/fixables/fixables.go
index 3966f6d1..ac3f2ffa 100644
--- a/internal/fixables/fixables.go
+++ b/internal/fixables/fixables.go
@@ -39,6 +39,7 @@ var All = []fixable.Fixable{
UnnecessaryArrowBlockFix,
UnnecessaryTypeofTypeFix,
PreferSchemaTypePropertyFix,
+ PreferSchemaUnionFix,
EffectMapVoidFix,
UnnecessaryFailYieldableErrorFix,
ClassSelfMismatchFix,
diff --git a/internal/fixables/prefer_schema_union.go b/internal/fixables/prefer_schema_union.go
new file mode 100644
index 00000000..f5b4c79f
--- /dev/null
+++ b/internal/fixables/prefer_schema_union.go
@@ -0,0 +1,343 @@
+package fixables
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/effect-ts/tsgo/internal/fixable"
+ "github.com/effect-ts/tsgo/internal/rewriter"
+ "github.com/effect-ts/tsgo/internal/rules"
+ "github.com/effect-ts/tsgo/internal/typeparser"
+ "github.com/microsoft/TypeScript/tsc/shim/ast"
+ "github.com/microsoft/TypeScript/tsc/shim/checker"
+ tsdiag "github.com/microsoft/TypeScript/tsc/shim/diagnostics"
+ "github.com/microsoft/TypeScript/tsc/shim/ls"
+ "github.com/microsoft/TypeScript/tsc/shim/scanner"
+)
+
+var PreferSchemaUnionFix = fixable.Fixable{
+ Name: "preferSchemaUnion",
+ Description: "Create Schema.Union and retain the type alias",
+ ErrorCodes: []int32{
+ tsdiag.This_type_alias_unions_decoded_Effect_Schema_types_Prefer_Schema_Union_and_derive_the_type_from_the_resulting_schema_effect_preferSchemaUnion.Code(),
+ },
+ FixIDs: []string{"preferSchemaUnion_fix"},
+ Run: runPreferSchemaUnionFix,
+}
+
+func runPreferSchemaUnionFix(ctx *fixable.Context) []ls.CodeAction {
+ matches := rules.AnalyzePreferSchemaUnion(ctx.TypeParser, ctx.SourceFile)
+ for _, match := range matches {
+ if !match.Location.Intersects(ctx.Span) && !ctx.Span.ContainedBy(match.Location) {
+ continue
+ }
+
+ if !isFixApplicable(ctx, match) {
+ continue
+ }
+
+ action := ctx.NewFixAction(fixable.FixAction{
+ Description: "Compose Schema.Union and derive type",
+ Run: func(tracker *rewriter.Tracker) {
+ applyPreferSchemaUnionFix(ctx, tracker, match)
+ },
+ })
+ if action != nil {
+ return []ls.CodeAction{*action}
+ }
+ return nil
+ }
+
+ return nil
+}
+
+func isFixApplicable(ctx *fixable.Context, match rules.PreferSchemaUnionMatch) bool {
+ if ctx == nil || ctx.SourceFile == nil || match.Alias == nil {
+ return false
+ }
+
+ // Limit offered fixes to top-level aliases in external modules
+ if !ast.IsExternalModule(ctx.SourceFile) {
+ return false
+ }
+ if match.Alias.Parent == nil || match.Alias.Parent.Kind != ast.KindSourceFile {
+ return false
+ }
+
+ aliasDecl := match.Alias.AsTypeAliasDeclaration()
+ if aliasDecl == nil || aliasDecl.Name() == nil || aliasDecl.Type == nil {
+ return false
+ }
+
+ // No ambient declarations
+ if match.Alias.Flags&ast.NodeFlagsAmbient != 0 || ast.HasAmbientModifier(match.Alias) {
+ return false
+ }
+
+ // No name collision for the new const in the value namespace
+ aliasName := aliasDecl.Name().Text()
+ if ctx.Checker.ResolveName(aliasName, match.Alias, ast.SymbolFlagsValue, false) != nil {
+ return false
+ }
+
+ // No comments inside the replaced union range
+ typeNode := aliasDecl.Type.AsNode()
+ if hasCommentsInRange(ctx.SourceFile, typeNode.Pos(), typeNode.End()) {
+ return false
+ }
+
+ // Schema leaves must be simple identifiers referencing same-file initialized const declarations or schema classes preceding the alias
+ if len(match.Schemas) == 0 {
+ return false
+ }
+ for _, schemaNode := range match.Schemas {
+ if schemaNode == nil || schemaNode.Kind != ast.KindIdentifier {
+ return false
+ }
+
+ sym := ctx.Checker.GetSymbolAtLocation(schemaNode)
+ if sym == nil {
+ return false
+ }
+ if sym.Flags&ast.SymbolFlagsAlias != 0 {
+ return false
+ }
+
+ if len(sym.Declarations) == 0 {
+ return false
+ }
+
+ hasValidDecl := false
+ for _, decl := range sym.Declarations {
+ if decl == nil {
+ continue
+ }
+ if ast.GetSourceFileOfNode(decl) != ctx.SourceFile {
+ return false
+ }
+ // Must strictly precede the alias (no forward or self references)
+ if decl.End() > match.Alias.Pos() {
+ return false
+ }
+ // Must not be ambient
+ if decl.Flags&ast.NodeFlagsAmbient != 0 || ast.HasAmbientModifier(decl) {
+ return false
+ }
+
+ if ast.IsVariableDeclaration(decl) {
+ vd := decl.AsVariableDeclaration()
+ if vd == nil || vd.Initializer == nil || !ast.IsVarConst(decl) {
+ return false
+ }
+ hasValidDecl = true
+ } else if ast.IsClassDeclaration(decl) {
+ hasValidDecl = true
+ }
+ }
+
+ if !hasValidDecl {
+ return false
+ }
+ }
+
+ return true
+}
+
+func hasCommentsInRange(sf *ast.SourceFile, pos, end int) bool {
+ if sf == nil {
+ return false
+ }
+ text := sf.Text()
+ if pos < 0 || end > len(text) || pos >= end {
+ return false
+ }
+ rangeText := text[pos:end]
+ return strings.Contains(rangeText, "//") || strings.Contains(rangeText, "/*")
+}
+
+func applyPreferSchemaUnionFix(ctx *fixable.Context, tracker *rewriter.Tracker, match rules.PreferSchemaUnionMatch) {
+ aliasDecl := match.Alias.AsTypeAliasDeclaration()
+ if aliasDecl == nil || aliasDecl.Name() == nil || aliasDecl.Type == nil {
+ return
+ }
+ aliasName := aliasDecl.Name().Text()
+
+ // 1. Resolve Schema value import
+ schemaModuleName, needImport := resolveSchemaImport(ctx, match.Alias)
+
+ if needImport {
+ var specifier *ast.Node
+ if schemaModuleName == "Schema" {
+ specifier = tracker.NewImportSpecifier(false, nil, tracker.NewIdentifier("Schema"))
+ } else {
+ specifier = tracker.NewImportSpecifier(false, tracker.NewIdentifier("Schema"), tracker.NewIdentifier(schemaModuleName))
+ }
+ namedImports := tracker.NewNamedImports(tracker.NewNodeList([]*ast.Node{specifier}))
+ importClause := tracker.NewImportClause(ast.KindUnknown, nil, namedImports)
+ moduleSpec := tracker.NewStringLiteral("effect", ast.TokenFlagsNone)
+ importDecl := tracker.NewImportDeclaration(nil, importClause, moduleSpec, nil)
+ ast.SetParentInChildren(importDecl)
+ tracker.InsertAtTopOfFile(ctx.SourceFile, []*ast.Statement{importDecl}, false)
+ }
+
+ // 2. Build Schema.Union([A, B])
+ schemaId := tracker.NewIdentifier(schemaModuleName)
+ callee := tracker.NewPropertyAccessExpression(
+ schemaId,
+ nil,
+ tracker.NewIdentifier("Union"),
+ ast.NodeFlagsNone,
+ )
+
+ arrayElements := make([]*ast.Node, 0, len(match.Schemas))
+ for _, schemaNode := range match.Schemas {
+ arrayElements = append(arrayElements, tracker.DeepCloneNode(schemaNode))
+ }
+ arrayLiteral := tracker.NewArrayLiteralExpression(tracker.NewNodeList(arrayElements), false)
+ callExpr := tracker.NewCallExpression(callee, nil, nil, tracker.NewNodeList([]*ast.Node{arrayLiteral}), ast.NodeFlagsNone)
+
+ // 3. Build const declaration
+ varDecl := tracker.NewVariableDeclaration(tracker.DeepCloneNode(aliasDecl.Name().AsNode()), nil, nil, callExpr)
+ varDeclList := tracker.NewVariableDeclarationList(tracker.NewNodeList([]*ast.Node{varDecl}), ast.NodeFlagsConst)
+
+ var modifierList *ast.ModifierList
+ if ast.HasSyntacticModifier(match.Alias, ast.ModifierFlagsExport) {
+ modifierList = tracker.NewModifierList([]*ast.Node{tracker.NewModifier(ast.KindExportKeyword)})
+ }
+ constStatement := tracker.NewVariableStatement(modifierList, varDeclList)
+ ast.SetParentInChildren(constStatement)
+
+ // 4. Insert const immediately before the alias
+ tracker.InsertNodeBefore(ctx.SourceFile, match.Alias, constStatement, false, rewriter.LeadingTriviaOptionNone)
+
+ // 5. Replace RHS of alias with typeof Alias.Type
+ schemaType := tracker.NewQualifiedName(tracker.NewIdentifier(aliasName), tracker.NewIdentifier("Type"))
+ rhsReplacement := tracker.NewTypeQueryNode(schemaType, nil)
+ ast.SetParentInChildren(rhsReplacement)
+ tracker.ReplaceNode(ctx.SourceFile, aliasDecl.Type.AsNode(), rhsReplacement, nil)
+}
+
+func resolveSchemaImport(ctx *fixable.Context, aliasNode *ast.Node) (string, bool) {
+ candidate := typeparser.FindModuleIdentifier(ctx.SourceFile, "Schema")
+ if isUsableSchemaImport(ctx, candidate) {
+ return candidate, false
+ }
+ return chooseUnboundSchemaName(ctx.Checker, aliasNode), true
+}
+
+func isUsableSchemaImport(ctx *fixable.Context, candidate string) bool {
+ if ctx == nil || ctx.SourceFile == nil || candidate == "" {
+ return false
+ }
+ for _, stmt := range ctx.SourceFile.Statements.Nodes {
+ if stmt.Kind != ast.KindImportDeclaration {
+ continue
+ }
+ importDecl := stmt.AsImportDeclaration()
+ if importDecl == nil || importDecl.ModuleSpecifier == nil || importDecl.ImportClause == nil {
+ continue
+ }
+ if importDecl.AsNode().IsTypeOnly() {
+ continue
+ }
+ clauseNode := importDecl.ImportClause.AsNode()
+ if clauseNode.IsTypeOnly() {
+ continue
+ }
+ clause := importDecl.ImportClause.AsImportClause()
+ if clause == nil {
+ continue
+ }
+
+ moduleName := scanner.GetTextOfNode(importDecl.ModuleSpecifier)
+ if len(moduleName) >= 2 && (moduleName[0] == '"' || moduleName[0] == '\'') {
+ moduleName = moduleName[1 : len(moduleName)-1]
+ }
+
+ if clause.NamedBindings == nil {
+ continue
+ }
+
+ if moduleName == "effect/Schema" && clause.NamedBindings.Kind == ast.KindNamespaceImport {
+ nsImport := clause.NamedBindings.AsNamespaceImport()
+ if nsImport != nil && nsImport.Name() != nil && nsImport.Name().Text() == candidate {
+ if !ast.IsTypeOnlyImportDeclaration(nsImport.AsNode()) {
+ if isSymbolFromEffectPackage(ctx, nsImport.Name().AsNode()) {
+ return true
+ }
+ }
+ }
+ }
+
+ if (moduleName == "effect" || moduleName == "effect/Schema") && clause.NamedBindings.Kind == ast.KindNamedImports {
+ namedImports := clause.NamedBindings.AsNamedImports()
+ if namedImports == nil || namedImports.Elements == nil {
+ continue
+ }
+ for _, elem := range namedImports.Elements.Nodes {
+ spec := elem.AsImportSpecifier()
+ if spec == nil || ast.IsTypeOnlyImportDeclaration(elem) {
+ continue
+ }
+ localName := spec.Name().Text()
+ importedName := localName
+ if spec.PropertyName != nil {
+ importedName = spec.PropertyName.Text()
+ }
+ if localName == candidate && importedName == "Schema" {
+ if isSymbolFromEffectPackage(ctx, spec.Name().AsNode()) {
+ return true
+ }
+ }
+ }
+ }
+ }
+ return false
+}
+
+func chooseUnboundSchemaName(c *checker.Checker, aliasNode *ast.Node) string {
+ if !isNameBound(c, aliasNode, "Schema") {
+ return "Schema"
+ }
+ name := "SchemaUnion"
+ suffix := 2
+ for isNameBound(c, aliasNode, name) {
+ name = fmt.Sprintf("SchemaUnion%d", suffix)
+ suffix++
+ }
+ return name
+}
+
+func isNameBound(c *checker.Checker, location *ast.Node, name string) bool {
+ if c == nil || location == nil {
+ return false
+ }
+ if c.ResolveName(name, location, ast.SymbolFlagsValue, false) != nil {
+ return true
+ }
+ if c.ResolveName(name, location, ast.SymbolFlagsType, false) != nil {
+ return true
+ }
+ return false
+}
+
+func isSymbolFromEffectPackage(ctx *fixable.Context, node *ast.Node) bool {
+ if ctx == nil || ctx.TypeParser == nil || ctx.Checker == nil || node == nil {
+ return false
+ }
+ sym := ctx.Checker.GetSymbolAtLocation(node)
+ if sym == nil {
+ return false
+ }
+ for sym != nil && sym.Flags&ast.SymbolFlagsAlias != 0 {
+ sym = ctx.Checker.GetAliasedSymbol(sym)
+ }
+ if sym == nil || len(sym.Declarations) == 0 {
+ return false
+ }
+ declSf := ast.GetSourceFileOfNode(sym.Declarations[0])
+ if declSf == nil {
+ return false
+ }
+ return ctx.TypeParser.IsSourceFileInPackage(declSf, "effect")
+}
diff --git a/internal/rules/prefer_schema_union.go b/internal/rules/prefer_schema_union.go
new file mode 100644
index 00000000..37de5e97
--- /dev/null
+++ b/internal/rules/prefer_schema_union.go
@@ -0,0 +1,181 @@
+package rules
+
+import (
+ "github.com/effect-ts/tsgo/etscore"
+ "github.com/effect-ts/tsgo/internal/rule"
+ "github.com/effect-ts/tsgo/internal/typeparser"
+ "github.com/microsoft/TypeScript/tsc/shim/ast"
+ "github.com/microsoft/TypeScript/tsc/shim/checker"
+ "github.com/microsoft/TypeScript/tsc/shim/core"
+ tsdiag "github.com/microsoft/TypeScript/tsc/shim/diagnostics"
+ "github.com/microsoft/TypeScript/tsc/shim/scanner"
+)
+
+var PreferSchemaUnion = rule.Rule{
+ Name: "preferSchemaUnion",
+ Group: "style",
+ Description: "Suggests composing Schema values with Schema.Union instead of unioning their extracted Type properties",
+ DefaultSeverity: etscore.SeverityOff,
+ SupportedEffect: []string{"v4"},
+ Codes: []int32{
+ tsdiag.This_type_alias_unions_decoded_Effect_Schema_types_Prefer_Schema_Union_and_derive_the_type_from_the_resulting_schema_effect_preferSchemaUnion.Code(),
+ },
+ Run: func(ctx *rule.Context) []*ast.Diagnostic {
+ matches := AnalyzePreferSchemaUnion(ctx.TypeParser, ctx.SourceFile)
+ diags := make([]*ast.Diagnostic, len(matches))
+ for i, match := range matches {
+ diags[i] = ctx.NewDiagnostic(
+ match.SourceFile,
+ match.Location,
+ tsdiag.This_type_alias_unions_decoded_Effect_Schema_types_Prefer_Schema_Union_and_derive_the_type_from_the_resulting_schema_effect_preferSchemaUnion,
+ nil,
+ )
+ }
+ return diags
+ },
+}
+
+type PreferSchemaUnionMatch struct {
+ SourceFile *ast.SourceFile
+ Location core.TextRange
+ Alias *ast.Node
+ Schemas []*ast.Node
+}
+
+func AnalyzePreferSchemaUnion(tp *typeparser.TypeParser, sf *ast.SourceFile) []PreferSchemaUnionMatch {
+ if tp == nil || sf == nil || sf.IsDeclarationFile {
+ return nil
+ }
+ if sf.ScriptKind != core.ScriptKindTS && sf.ScriptKind != core.ScriptKindTSX && sf.ScriptKind != core.ScriptKindUnknown {
+ return nil
+ }
+ if tp.DetectEffectVersion() != typeparser.EffectMajorV4 {
+ return nil
+ }
+
+ var matches []PreferSchemaUnionMatch
+ var walk ast.Visitor
+ walk = func(node *ast.Node) bool {
+ if node == nil {
+ return false
+ }
+ if node.Kind == ast.KindTypeAliasDeclaration {
+ if match := analyzePreferSchemaUnionAlias(tp, sf, node); match != nil {
+ matches = append(matches, *match)
+ }
+ }
+ node.ForEachChild(walk)
+ return false
+ }
+ walk(sf.AsNode())
+ return matches
+}
+
+func analyzePreferSchemaUnionAlias(tp *typeparser.TypeParser, sf *ast.SourceFile, node *ast.Node) *PreferSchemaUnionMatch {
+ if node.Flags&ast.NodeFlagsAmbient != 0 || ast.HasAmbientModifier(node) {
+ return nil
+ }
+
+ ta := node.AsTypeAliasDeclaration()
+ if ta == nil || ta.Type == nil {
+ return nil
+ }
+ if ta.TypeParameters != nil && len(ta.TypeParameters.Nodes) > 0 {
+ return nil
+ }
+
+ unwrapped := ast.SkipTypeParentheses(ta.Type.AsNode())
+ if unwrapped == nil || unwrapped.Kind != ast.KindUnionType {
+ return nil
+ }
+
+ leaves := collectUnionLeaves(unwrapped)
+ if len(leaves) < 2 {
+ return nil
+ }
+
+ schemas := make([]*ast.Node, 0, len(leaves))
+ for _, leaf := range leaves {
+ if leaf.Kind != ast.KindTypeQuery {
+ return nil
+ }
+ query := leaf.AsTypeQueryNode()
+ if query == nil || query.ExprName == nil {
+ return nil
+ }
+ if query.TypeArguments != nil && len(query.TypeArguments.Nodes) > 0 {
+ return nil
+ }
+
+ exprName := query.ExprName.AsNode()
+ if exprName == nil || exprName.Kind != ast.KindQualifiedName {
+ return nil
+ }
+ qual := exprName.AsQualifiedName()
+ if qual.Right == nil || qual.Right.Text() != "Type" || qual.Left == nil {
+ return nil
+ }
+
+ schemaNode := qual.Left.AsNode()
+ t := getSchemaNodeType(tp, schemaNode)
+ if t == nil || t.Flags()&checker.TypeFlagsAnyOrUnknown != 0 {
+ return nil
+ }
+ if !tp.IsSchemaType(t) {
+ return nil
+ }
+
+ schemas = append(schemas, schemaNode)
+ }
+
+ return &PreferSchemaUnionMatch{
+ SourceFile: sf,
+ Location: scanner.GetErrorRangeForNode(sf, ta.Type.AsNode()),
+ Alias: node,
+ Schemas: schemas,
+ }
+}
+
+func collectUnionLeaves(node *ast.Node) []*ast.Node {
+ node = ast.SkipTypeParentheses(node)
+ if node == nil {
+ return nil
+ }
+ if node.Kind == ast.KindUnionType {
+ ut := node.AsUnionTypeNode()
+ if ut == nil || ut.Types == nil {
+ return nil
+ }
+ var leaves []*ast.Node
+ for _, child := range ut.Types.Nodes {
+ childLeaves := collectUnionLeaves(child)
+ if childLeaves == nil {
+ return nil
+ }
+ leaves = append(leaves, childLeaves...)
+ }
+ return leaves
+ }
+ return []*ast.Node{node}
+}
+
+func getSchemaNodeType(tp *typeparser.TypeParser, node *ast.Node) *checker.Type {
+ if tp == nil || node == nil {
+ return nil
+ }
+ t := tp.GetTypeAtLocation(node)
+ if t != nil && t.Flags()&checker.TypeFlagsAnyOrUnknown == 0 {
+ return t
+ }
+ sym := tp.GetSymbolAtLocation(node)
+ if sym == nil && node.Kind == ast.KindQualifiedName {
+ qual := node.AsQualifiedName()
+ if qual.Right != nil {
+ sym = tp.GetSymbolAtLocation(qual.Right.AsNode())
+ }
+ }
+ if sym != nil {
+ return tp.GetTypeOfSymbolAtLocation(sym, node)
+ }
+ return nil
+}
diff --git a/internal/rules/rules.go b/internal/rules/rules.go
index ccaa3bee..b666223f 100644
--- a/internal/rules/rules.go
+++ b/internal/rules/rules.go
@@ -86,6 +86,7 @@ var All = []rule.Rule{
UnnecessaryArrowBlock,
UnnecessaryTypeofType,
PreferSchemaTypeProperty,
+ PreferSchemaUnion,
InstanceOfSchema,
GenericEffectServices,
OverriddenSchemaConstructor,
diff --git a/internal/typeparser/helpers.go b/internal/typeparser/helpers.go
index 9f77237f..d57a0af3 100644
--- a/internal/typeparser/helpers.go
+++ b/internal/typeparser/helpers.go
@@ -82,6 +82,13 @@ func (tp *TypeParser) GetSymbolAtLocation(node *ast.Node) *ast.Symbol {
return tp.checker.GetSymbolAtLocation(node)
}
+func (tp *TypeParser) GetTypeOfSymbolAtLocation(sym *ast.Symbol, node *ast.Node) *checker.Type {
+ if tp == nil || tp.checker == nil || sym == nil {
+ return nil
+ }
+ return tp.checker.GetTypeOfSymbolAtLocation(sym, node)
+}
+
func (tp *TypeParser) resolveAliasedSymbol(sym *ast.Symbol) *ast.Symbol {
if tp == nil || tp.checker == nil {
return sym
diff --git a/internal/typeparser/schema_type.go b/internal/typeparser/schema_type.go
index e00b5b07..f938876c 100644
--- a/internal/typeparser/schema_type.go
+++ b/internal/typeparser/schema_type.go
@@ -110,6 +110,11 @@ func (tp *TypeParser) IsNodeReferenceToEffectSchemaModuleApi(node *ast.Node, mem
return tp.IsNodeReferenceToModuleExport(node, effectSchemaModuleDescriptor, memberName)
}
+// IsNodeReferenceToEffectSchemaModule reports whether node resolves to the Effect Schema module.
+func (tp *TypeParser) IsNodeReferenceToEffectSchemaModule(node *ast.Node) bool {
+ return tp.IsNodeReferenceToModule(node, effectSchemaModuleDescriptor)
+}
+
func isParseResultSourceFile(_ *TypeParser, c *checker.Checker, sf *ast.SourceFile) bool {
if c == nil || sf == nil {
return false
diff --git a/shim/diagnostics/shim.go b/shim/diagnostics/shim.go
index f93ae1fd..8b6d0bac 100644
--- a/shim/diagnostics/shim.go
+++ b/shim/diagnostics/shim.go
@@ -1922,6 +1922,7 @@ var This_syntax_is_reserved_in_files_with_the_mts_or_cts_extension_Use_an_as_exp
var This_syntax_requires_an_imported_helper_but_module_0_cannot_be_found = diagnostics.This_syntax_requires_an_imported_helper_but_module_0_cannot_be_found
var This_syntax_requires_an_imported_helper_named_1_which_does_not_exist_in_0_Consider_upgrading_your_version_of_0 = diagnostics.This_syntax_requires_an_imported_helper_named_1_which_does_not_exist_in_0_Consider_upgrading_your_version_of_0
var This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0 = diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0
+var This_type_alias_unions_decoded_Effect_Schema_types_Prefer_Schema_Union_and_derive_the_type_from_the_resulting_schema_effect_preferSchemaUnion = diagnostics.This_type_alias_unions_decoded_Effect_Schema_types_Prefer_Schema_Union_and_derive_the_type_from_the_resulting_schema_effect_preferSchemaUnion
var This_type_assertion_unsafely_narrows_the_error_or_requirements_channels_effect_unsafeEffectTypeAssertion = diagnostics.This_type_assertion_unsafely_narrows_the_error_or_requirements_channels_effect_unsafeEffectTypeAssertion
var This_type_parameter_might_need_an_extends_0_constraint = diagnostics.This_type_parameter_might_need_an_extends_0_constraint
var This_typeof_Type_query_can_be_replaced_with_0_effect_unnecessaryTypeofType = diagnostics.This_typeof_Type_query_can_be_replaced_with_0_effect_unnecessaryTypeofType
diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.errors.txt b/testdata/baselines/reference/effect-v3/preferSchemaUnion.errors.txt
new file mode 100644
index 00000000..a0fe4aef
--- /dev/null
+++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.errors.txt
@@ -0,0 +1,16 @@
+=== Metadata ===
+Effect version: 3.19.19
+
+
+
+==== /.src/preferSchemaUnion.ts (0 errors) ====
+ // @effect-v3
+ // @effect-diagnostics *:off
+ // @effect-diagnostics preferSchemaUnion:warning
+ import { Schema } from "effect"
+
+ const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+ const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+
+ export type Shape = typeof Circle.Type | typeof Square.Type
+
diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.preferSchemaUnion.mermaid b/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.preferSchemaUnion.mermaid
new file mode 100644
index 00000000..6ffd975e
--- /dev/null
+++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.preferSchemaUnion.mermaid
@@ -0,0 +1,5 @@
+flowchart TB
+ 0[/"type: Struct#lt;#123; kind: Literal#lt;#91;#quot;circle#quot;#93;#gt;; radius: typeof Number$; #125;#gt;
node: Schema.Struct#40;#123; kind: Schema.Literal#40;#quot;circle#quot;#41;, radius: Schema.Number #125;#41;"/]
+ 1[/"type: Struct#lt;#123; kind: Literal#lt;#91;#quot;square#quot;#93;#gt;; side: typeof Number$; #125;#gt;
node: Schema.Struct#40;#123; kind: Schema.Literal#40;#quot;square#quot;#41;, side: Schema.Number #125;#41;"/]
+ 2[/"type:
node: Circle.Type"/]
+ 3[/"type:
node: Square.Type"/]
\ No newline at end of file
diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.txt b/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.txt
new file mode 100644
index 00000000..05b8fe3e
--- /dev/null
+++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.txt
@@ -0,0 +1 @@
+/.src/preferSchemaUnion.ts -> preferSchemaUnion.flows.preferSchemaUnion.mermaid
diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.layers.txt b/testdata/baselines/reference/effect-v3/preferSchemaUnion.layers.txt
new file mode 100644
index 00000000..cfd7e2d8
--- /dev/null
+++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.layers.txt
@@ -0,0 +1 @@
+==== /.src/preferSchemaUnion.ts (0 layer exports) ====
diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.pipings.txt b/testdata/baselines/reference/effect-v3/preferSchemaUnion.pipings.txt
new file mode 100644
index 00000000..df01f611
--- /dev/null
+++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.pipings.txt
@@ -0,0 +1,57 @@
+==== /.src/preferSchemaUnion.ts (4 flows) ====
+
+=== Piping Flow ===
+Location: 6:15 - 6:88
+Node: Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+Node Kind: KindCallExpression
+
+Subject: { kind: Schema.Literal("circle"), radius: Schema.Number }
+Subject Type: { kind: Literal<["circle"]>; radius: typeof Number$; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Struct
+ args: (constant)
+ outType: Struct<{ kind: Literal<["circle"]>; radius: typeof Number$; }>
+
+=== Piping Flow ===
+Location: 6:37 - 6:62
+Node: Schema.Literal("circle")
+Node Kind: KindCallExpression
+
+Subject: "circle"
+Subject Type: "circle"
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Literal
+ args: (constant)
+ outType: Literal<["circle"]>
+
+=== Piping Flow ===
+Location: 7:15 - 7:86
+Node: Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+Node Kind: KindCallExpression
+
+Subject: { kind: Schema.Literal("square"), side: Schema.Number }
+Subject Type: { kind: Literal<["square"]>; side: typeof Number$; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Struct
+ args: (constant)
+ outType: Struct<{ kind: Literal<["square"]>; side: typeof Number$; }>
+
+=== Piping Flow ===
+Location: 7:37 - 7:62
+Node: Schema.Literal("square")
+Node Kind: KindCallExpression
+
+Subject: "square"
+Subject Type: "square"
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Literal
+ args: (constant)
+ outType: Literal<["square"]>
diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.quickfixes.txt b/testdata/baselines/reference/effect-v3/preferSchemaUnion.quickfixes.txt
new file mode 100644
index 00000000..dad05bfc
--- /dev/null
+++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.quickfixes.txt
@@ -0,0 +1,5 @@
+=== Quick Fix Inventory ===
+(no diagnostics)
+
+=== Quick Fix Application Results ===
+(no quick fixes to apply)
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.errors.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion.errors.txt
new file mode 100644
index 00000000..c54f12d7
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.errors.txt
@@ -0,0 +1,99 @@
+=== Metadata ===
+Effect version: 4.0.0
+
+/.src/preferSchemaUnion.ts(10,22): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+/.src/preferSchemaUnion.ts(11,22): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+/.src/preferSchemaUnion.ts(21,29): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+/.src/preferSchemaUnion.ts(44,30): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+/.src/preferSchemaUnion.ts(46,28): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+/.src/preferSchemaUnion.ts(53,31): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+/.src/preferSchemaUnion.ts(56,28): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+/.src/preferSchemaUnion.ts(60,3): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+/.src/preferSchemaUnion.ts(65,28): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+
+
+==== /.src/preferSchemaUnion.ts (9 errors) ====
+ // @effect-diagnostics *:off
+ // @effect-diagnostics preferSchemaUnion:warning
+ import { Schema, Schema as S } from "effect"
+
+ // 1. Two and three concrete schema leaves, including parenthesized unions
+ export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+ export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+ export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number })
+
+ export type Shape2 = typeof Circle.Type | typeof Square.Type
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ export type Shape3 = (typeof Circle.Type | typeof Square.Type) | typeof Triangle.Type
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+
+ // 2. Schema.Class, refinements and transformed codecs
+ export class UserClass extends Schema.Class("UserClass")({
+ id: Schema.String,
+ name: Schema.String
+ }) {}
+ export const NonEmptyString = Schema.NonEmptyString
+ export const NumberFromString = Schema.NumberFromString
+
+ export type ValidEntities = typeof UserClass.Type | typeof NonEmptyString.Type | typeof NumberFromString.Type
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+
+ // 3. Negative cases - NO DIAGNOSTIC
+ type MixedPrimitive = typeof Circle.Type | null
+ type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded
+ const Lookalike = { Type: "lookalike" as const }
+ type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type
+ declare const AnyReceiver: any
+ type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type
+ type GenericAlias = typeof Circle.Type | typeof Square.Type
+ declare type AmbientAlias = typeof Circle.Type | typeof Square.Type
+ type SingleMember = typeof Circle.Type
+ type NarrowedSchemaWithoutEncoded = {
+ readonly "~effect/Schema/Schema": true
+ readonly Type: string
+ }
+ declare const Narrowed: NarrowedSchemaWithoutEncoded
+ type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type
+ export const AlreadyComposed = Schema.Union([Circle, Square])
+ export type AlreadyComposed = typeof AlreadyComposed.Type
+
+ // 4. Diagnosed, but safety checks withhold code action
+ export const CollidingValue = 42
+ export type CollidingValue = typeof Circle.Type | typeof Square.Type
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+
+ export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ export const ForwardA = Schema.Struct({ a: Schema.String })
+ export const ForwardB = Schema.Struct({ b: Schema.String })
+
+ namespace Models {
+ export const SubItem = Schema.String
+ }
+ export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+
+ let MutableSchema = Schema.Struct({ m: Schema.String })
+ export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+
+ export type CommentedUnion =
+ // interior comment
+ typeof Circle.Type | typeof Square.Type
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+
+ // 5. Existing aliased Schema import
+ export const A = S.Struct({ a: S.String })
+ export const B = S.Struct({ b: S.Number })
+ export type AliasedUnion = typeof A.Type | typeof B.Type
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.preferSchemaUnion.mermaid b/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.preferSchemaUnion.mermaid
new file mode 100644
index 00000000..76b0ae0d
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.preferSchemaUnion.mermaid
@@ -0,0 +1,109 @@
+flowchart TB
+ 0[/"type: #123; kind: Literal#lt;#quot;circle#quot;#gt;; radius: Number; #125;
node: #123; kind: Schema.Literal#40;#quot;circle#quot;#41;, radius: Schema.Number #125;"/]
+ 1[/"type: #quot;circle#quot;
node: #quot;circle#quot;"/]
+ 2["type: Literal#lt;#quot;circle#quot;#gt;
callee: Schema.Literal
args: #91;#93;"]
+ 3[/"type: #lt;L extends SchemaAST.LiteralValue#gt;#40;literal: L#41; =#gt; Literal#lt;L#gt;
node: Schema.Literal"/]
+ 4["type: Struct#lt;#123; readonly kind: Literal#lt;#quot;circle#quot;#gt;; readonly radius: Number; #125;#gt;
callee: Schema.Struct
args: #91;#93;"]
+ 5[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/]
+ 6[/"type: #123; kind: Literal#lt;#quot;square#quot;#gt;; side: Number; #125;
node: #123; kind: Schema.Literal#40;#quot;square#quot;#41;, side: Schema.Number #125;"/]
+ 7[/"type: #quot;square#quot;
node: #quot;square#quot;"/]
+ 8["type: Literal#lt;#quot;square#quot;#gt;
callee: Schema.Literal
args: #91;#93;"]
+ 9[/"type: #lt;L extends SchemaAST.LiteralValue#gt;#40;literal: L#41; =#gt; Literal#lt;L#gt;
node: Schema.Literal"/]
+ 10["type: Struct#lt;#123; readonly kind: Literal#lt;#quot;square#quot;#gt;; readonly side: Number; #125;#gt;
callee: Schema.Struct
args: #91;#93;"]
+ 11[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/]
+ 12[/"type: #123; kind: Literal#lt;#quot;triangle#quot;#gt;; base: Number; height: Number; #125;
node: #123; kind: Schema.Literal#40;#quot;triangle#quot;#41;, base: Schema.Number, height: Schema.Number #125;"/]
+ 13[/"type: #quot;triangle#quot;
node: #quot;triangle#quot;"/]
+ 14["type: Literal#lt;#quot;triangle#quot;#gt;
callee: Schema.Literal
args: #91;#93;"]
+ 15[/"type: #lt;L extends SchemaAST.LiteralValue#gt;#40;literal: L#41; =#gt; Literal#lt;L#gt;
node: Schema.Literal"/]
+ 16["type: Struct#lt;#123; readonly kind: Literal#lt;#quot;triangle#quot;#gt;; readonly base: Number; readonly height: Number; #125;#gt;
callee: Schema.Struct
args: #91;#93;"]
+ 17[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/]
+ 18[/"type:
node: Circle.Type"/]
+ 19[/"type:
node: Square.Type"/]
+ 20[/"type:
node: Circle.Type"/]
+ 21[/"type:
node: Square.Type"/]
+ 22[/"type:
node: Triangle.Type"/]
+ 23[/"type: Class#lt;UserClass, Struct#lt;#123; readonly id: String; readonly name: String; #125;#gt;, #123;#125;#gt;
node: Schema.Class#lt;UserClass#gt;#40;#quot;UserClass#quot;#41;#40;#123;#92;n id: Schema.String,#92;n name: Schema.String#92;n#125;#41;"/]
+ 24[/"type: #quot;UserClass#quot;
node: #quot;UserClass#quot;"/]
+ 25["type: #123; #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields, annotations?: Declaration#lt;UserClass, readonly #91;Struct#lt;Fields#gt;#93;#gt; #124; undefined#41;: Class#lt;UserClass, Struct#lt;Fields#gt;, #123;#125;#gt;; #lt;S extends Struct#lt;Struct.Fields#gt;#gt;#40;schema: S, annotations?: Declaration#lt;UserClass, readonly #91;S#93;#gt; #124; undefined#41;: Class#lt;UserClass, S, #123;#125;#gt;; #125;
callee: Schema.Class
args: #91;#93;"]
+ 26[/"type: #lt;Self = never, Brand = #123;#125;#gt;#40;identifier: string#41; =#gt; #123; #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields, annotations?: Declaration#lt;Self, readonly #91;Struct#lt;Fields#gt;#93;#gt; #124; undefined#41;: #91;Self#93; extends #91;never#93; ? #quot;Missing `Self` generic - use `class Self extends Schema.Class#lt;Self#gt;#40;...#41;`#quot; : Class#lt;Self, Struct#lt;Fields#gt;, Brand#gt;; #lt;S extends Struct#lt;Struct.Fields#gt;#gt;#40;schema: S, annotations?: Declaration#lt;Self, readonly #91;S#93;#gt; #124; undefined#41;: #91;Self#93; extends #91;never#93; ? #quot;Missing `Self` generic - use `class Self extends Schema.Class#lt;Self#gt;#40;...#41;`#quot; : Class#lt;Self, S, Brand#gt;; #125;
node: Schema.Class"/]
+ 27[/"type: NonEmptyString
node: Schema.NonEmptyString"/]
+ 28[/"type: NumberFromString
node: Schema.NumberFromString"/]
+ 29[/"type:
node: UserClass.Type"/]
+ 30[/"type:
node: NonEmptyString.Type"/]
+ 31[/"type:
node: NumberFromString.Type"/]
+ 32[/"type:
node: Circle.Type"/]
+ 33[/"type: null
node: null"/]
+ 34[/"type:
node: Circle.Encoded"/]
+ 35[/"type:
node: Square.Encoded"/]
+ 36[/"type: #123; Type: #quot;lookalike#quot;; #125;
node: #123; Type: #quot;lookalike#quot; as const #125;"/]
+ 37[/"type:
node: Lookalike.Type"/]
+ 38[/"type:
node: Circle.Type"/]
+ 39[/"type:
node: AnyReceiver.Type"/]
+ 40[/"type:
node: Circle.Type"/]
+ 41[/"type:
node: Circle.Type"/]
+ 42[/"type:
node: Square.Type"/]
+ 43[/"type:
node: Circle.Type"/]
+ 44[/"type:
node: Square.Type"/]
+ 45[/"type:
node: Circle.Type"/]
+ 46[/"type: true
node: true"/]
+ 47[/"type:
node: Narrowed.Type"/]
+ 48[/"type:
node: Circle.Type"/]
+ 49[/"type: Union#lt;readonly #91;Struct#lt;#123; readonly kind: Literal#lt;#quot;circle#quot;#gt;; readonly radius: Number; #125;#gt;, Struct#lt;#123; readonly kind: Literal#lt;#quot;square#quot;#gt;; readonly side: Number; #125;#gt;#93;#gt;
node: Schema.Union#40;#91;Circle, Square#93;#41;"/]
+ 50[/"type:
node: AlreadyComposed.Type"/]
+ 51[/"type: 42
node: 42"/]
+ 52[/"type:
node: Circle.Type"/]
+ 53[/"type:
node: Square.Type"/]
+ 54[/"type:
node: ForwardA.Type"/]
+ 55[/"type:
node: ForwardB.Type"/]
+ 56[/"type: #123; a: String; #125;
node: #123; a: Schema.String #125;"/]
+ 57["type: Struct#lt;#123; readonly a: String; #125;#gt;
callee: Schema.Struct
args: #91;#93;"]
+ 58[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/]
+ 59[/"type: #123; b: String; #125;
node: #123; b: Schema.String #125;"/]
+ 60["type: Struct#lt;#123; readonly b: String; #125;#gt;
callee: Schema.Struct
args: #91;#93;"]
+ 61[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/]
+ 62[/"type: String
node: Schema.String"/]
+ 63[/"type:
node: Models.SubItem.Type"/]
+ 64[/"type:
node: Circle.Type"/]
+ 65[/"type: #123; m: String; #125;
node: #123; m: Schema.String #125;"/]
+ 66["type: Struct#lt;#123; readonly m: String; #125;#gt;
callee: Schema.Struct
args: #91;#93;"]
+ 67[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/]
+ 68[/"type:
node: MutableSchema.Type"/]
+ 69[/"type:
node: Circle.Type"/]
+ 70[/"type:
node: Circle.Type"/]
+ 71[/"type:
node: Square.Type"/]
+ 72[/"type: #123; a: String; #125;
node: #123; a: S.String #125;"/]
+ 73["type: Struct#lt;#123; readonly a: String; #125;#gt;
callee: S.Struct
args: #91;#93;"]
+ 74[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: S.Struct"/]
+ 75[/"type: #123; b: Number; #125;
node: #123; b: S.Number #125;"/]
+ 76["type: Struct#lt;#123; readonly b: Number; #125;#gt;
callee: S.Struct
args: #91;#93;"]
+ 77[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: S.Struct"/]
+ 78[/"type:
node: A.Type"/]
+ 79[/"type:
node: B.Type"/]
+ 1 -->|"kind: pipe"| 2
+ 3 -->|"kind: transformCallee"| 2
+ 2 -->|"kind: usedBy"| 0
+ 0 -->|"kind: pipe"| 4
+ 5 -->|"kind: transformCallee"| 4
+ 7 -->|"kind: pipe"| 8
+ 9 -->|"kind: transformCallee"| 8
+ 8 -->|"kind: usedBy"| 6
+ 6 -->|"kind: pipe"| 10
+ 11 -->|"kind: transformCallee"| 10
+ 13 -->|"kind: pipe"| 14
+ 15 -->|"kind: transformCallee"| 14
+ 14 -->|"kind: usedBy"| 12
+ 12 -->|"kind: pipe"| 16
+ 17 -->|"kind: transformCallee"| 16
+ 24 -->|"kind: pipe"| 25
+ 26 -->|"kind: transformCallee"| 25
+ 25 -->|"kind: usedBy"| 23
+ 56 -->|"kind: pipe"| 57
+ 58 -->|"kind: transformCallee"| 57
+ 59 -->|"kind: pipe"| 60
+ 61 -->|"kind: transformCallee"| 60
+ 65 -->|"kind: pipe"| 66
+ 67 -->|"kind: transformCallee"| 66
+ 72 -->|"kind: pipe"| 73
+ 74 -->|"kind: transformCallee"| 73
+ 75 -->|"kind: pipe"| 76
+ 77 -->|"kind: transformCallee"| 76
\ No newline at end of file
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.txt
new file mode 100644
index 00000000..05b8fe3e
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.txt
@@ -0,0 +1 @@
+/.src/preferSchemaUnion.ts -> preferSchemaUnion.flows.preferSchemaUnion.mermaid
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.layers.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion.layers.txt
new file mode 100644
index 00000000..cfd7e2d8
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.layers.txt
@@ -0,0 +1 @@
+==== /.src/preferSchemaUnion.ts (0 layer exports) ====
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.pipings.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion.pipings.txt
new file mode 100644
index 00000000..c666d906
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.pipings.txt
@@ -0,0 +1,183 @@
+==== /.src/preferSchemaUnion.ts (13 flows) ====
+
+=== Piping Flow ===
+Location: 6:22 - 6:95
+Node: Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+Node Kind: KindCallExpression
+
+Subject: { kind: Schema.Literal("circle"), radius: Schema.Number }
+Subject Type: { kind: Literal<"circle">; radius: Number; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Struct
+ args: (constant)
+ outType: Struct<{ readonly kind: Literal<"circle">; readonly radius: Number; }>
+
+=== Piping Flow ===
+Location: 6:44 - 6:69
+Node: Schema.Literal("circle")
+Node Kind: KindCallExpression
+
+Subject: "circle"
+Subject Type: "circle"
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Literal
+ args: (constant)
+ outType: Literal<"circle">
+
+=== Piping Flow ===
+Location: 7:22 - 7:93
+Node: Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+Node Kind: KindCallExpression
+
+Subject: { kind: Schema.Literal("square"), side: Schema.Number }
+Subject Type: { kind: Literal<"square">; side: Number; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Struct
+ args: (constant)
+ outType: Struct<{ readonly kind: Literal<"square">; readonly side: Number; }>
+
+=== Piping Flow ===
+Location: 7:44 - 7:69
+Node: Schema.Literal("square")
+Node Kind: KindCallExpression
+
+Subject: "square"
+Subject Type: "square"
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Literal
+ args: (constant)
+ outType: Literal<"square">
+
+=== Piping Flow ===
+Location: 8:24 - 8:120
+Node: Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number })
+Node Kind: KindCallExpression
+
+Subject: { kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number }
+Subject Type: { kind: Literal<"triangle">; base: Number; height: Number; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Struct
+ args: (constant)
+ outType: Struct<{ readonly kind: Literal<"triangle">; readonly base: Number; readonly height: Number; }>
+
+=== Piping Flow ===
+Location: 8:46 - 8:73
+Node: Schema.Literal("triangle")
+Node Kind: KindCallExpression
+
+Subject: "triangle"
+Subject Type: "triangle"
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Literal
+ args: (constant)
+ outType: Literal<"triangle">
+
+=== Piping Flow ===
+Location: 14:31 - 17:3
+Node: Schema.Class("UserClass")({\n id: Schema.String,\n name: Schema.String\n})
+Node Kind: KindCallExpression
+
+Subject: {\n id: Schema.String,\n name: Schema.String\n}
+Subject Type: { id: String; name: String; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Class("UserClass")
+ args: (constant)
+ outType: Class, {}>
+
+=== Piping Flow ===
+Location: 39:31 - 39:62
+Node: Schema.Union([Circle, Square])
+Node Kind: KindCallExpression
+
+Subject: [Circle, Square]
+Subject Type: [Struct<{ readonly kind: Literal<"circle">; readonly radius: Number; }>, Struct<{ readonly kind: Literal<"square">; readonly side: Number; }>]
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Union
+ args: (constant)
+ outType: Union; readonly radius: Number; }>, Struct<{ readonly kind: Literal<"square">; readonly side: Number; }>]>
+
+=== Piping Flow ===
+Location: 47:24 - 47:60
+Node: Schema.Struct({ a: Schema.String })
+Node Kind: KindCallExpression
+
+Subject: { a: Schema.String }
+Subject Type: { a: String; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Struct
+ args: (constant)
+ outType: Struct<{ readonly a: String; }>
+
+=== Piping Flow ===
+Location: 48:24 - 48:60
+Node: Schema.Struct({ b: Schema.String })
+Node Kind: KindCallExpression
+
+Subject: { b: Schema.String }
+Subject Type: { b: String; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Struct
+ args: (constant)
+ outType: Struct<{ readonly b: String; }>
+
+=== Piping Flow ===
+Location: 55:20 - 55:56
+Node: Schema.Struct({ m: Schema.String })
+Node Kind: KindCallExpression
+
+Subject: { m: Schema.String }
+Subject Type: { m: String; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Struct
+ args: (constant)
+ outType: Struct<{ readonly m: String; }>
+
+=== Piping Flow ===
+Location: 63:17 - 63:43
+Node: S.Struct({ a: S.String })
+Node Kind: KindCallExpression
+
+Subject: { a: S.String }
+Subject Type: { a: String; }
+
+Transformations (1):
+ [0] kind: call
+ callee: S.Struct
+ args: (constant)
+ outType: Struct<{ readonly a: String; }>
+
+=== Piping Flow ===
+Location: 64:17 - 64:43
+Node: S.Struct({ b: S.Number })
+Node Kind: KindCallExpression
+
+Subject: { b: S.Number }
+Subject Type: { b: Number; }
+
+Transformations (1):
+ [0] kind: call
+ callee: S.Struct
+ args: (constant)
+ outType: Struct<{ readonly b: Number; }>
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.quickfixes.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion.quickfixes.txt
new file mode 100644
index 00000000..0323dc91
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.quickfixes.txt
@@ -0,0 +1,405 @@
+=== Quick Fix Inventory ===
+
+[D1] (10:22-10:61) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ Fix 0: "Disable preferSchemaUnion for this line"
+ Fix 1: "Disable preferSchemaUnion for entire file"
+ Fix 2: "Compose Schema.Union and derive type"
+
+[D2] (11:22-11:86) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ Fix 0: "Disable preferSchemaUnion for this line"
+ Fix 1: "Disable preferSchemaUnion for entire file"
+ Fix 2: "Compose Schema.Union and derive type"
+
+[D3] (21:29-21:110) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ Fix 0: "Disable preferSchemaUnion for this line"
+ Fix 1: "Disable preferSchemaUnion for entire file"
+ Fix 2: "Compose Schema.Union and derive type"
+
+[D4] (24:6-24:20) TS6196: 'MixedPrimitive' is declared but never used.
+ (no quick fixes)
+
+[D5] (25:6-25:18) TS6196: 'EncodedUnion' is declared but never used.
+ (no quick fixes)
+
+[D6] (27:6-27:20) TS6196: 'LookalikeUnion' is declared but never used.
+ (no quick fixes)
+
+[D7] (29:6-29:14) TS6196: 'AnyUnion' is declared but never used.
+ (no quick fixes)
+
+[D8] (30:6-30:18) TS6196: 'GenericAlias' is declared but never used.
+ (no quick fixes)
+
+[D9] (30:19-30:20) TS6196: 'T' is declared but never used.
+ (no quick fixes)
+
+[D10] (32:6-32:18) TS6196: 'SingleMember' is declared but never used.
+ (no quick fixes)
+
+[D11] (38:6-38:19) TS6196: 'NarrowedUnion' is declared but never used.
+ (no quick fixes)
+
+[D12] (44:30-44:69) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ Fix 0: "Disable preferSchemaUnion for this line"
+ Fix 1: "Disable preferSchemaUnion for entire file"
+
+[D13] (46:28-46:71) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ Fix 0: "Disable preferSchemaUnion for this line"
+ Fix 1: "Disable preferSchemaUnion for entire file"
+
+[D14] (53:31-53:78) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ Fix 0: "Disable preferSchemaUnion for this line"
+ Fix 1: "Disable preferSchemaUnion for entire file"
+
+[D15] (56:28-56:74) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ Fix 0: "Disable preferSchemaUnion for this line"
+ Fix 1: "Disable preferSchemaUnion for entire file"
+
+[D16] (60:3-60:42) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ Fix 0: "Disable preferSchemaUnion for this line"
+ Fix 1: "Disable preferSchemaUnion for entire file"
+
+[D17] (65:28-65:57) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ Fix 0: "Disable preferSchemaUnion for this line"
+ Fix 1: "Disable preferSchemaUnion for entire file"
+ Fix 2: "Compose Schema.Union and derive type"
+
+=== Quick Fix Application Results ===
+
+=== [D1] Fix 0: "Disable preferSchemaUnion for this line" ===
+skipped by default
+
+=== [D1] Fix 1: "Disable preferSchemaUnion for entire file" ===
+skipped by default
+
+=== [D1] Fix 2: "Compose Schema.Union and derive type" ===
+
+--- file:///.src/preferSchemaUnion.ts ---
+// @effect-diagnostics *:off
+// @effect-diagnostics preferSchemaUnion:warning
+import { Schema, Schema as S } from "effect"
+
+// 1. Two and three concrete schema leaves, including parenthesized unions
+export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number })
+
+export const Shape2 = Schema.Union([Circle, Square]);
+export type Shape2 = typeof Shape2.Type
+export type Shape3 = (typeof Circle.Type | typeof Square.Type) | typeof Triangle.Type
+
+// 2. Schema.Class, refinements and transformed codecs
+export class UserClass extends Schema.Class("UserClass")({
+ id: Schema.String,
+ name: Schema.String
+}) {}
+export const NonEmptyString = Schema.NonEmptyString
+export const NumberFromString = Schema.NumberFromString
+
+export type ValidEntities = typeof UserClass.Type | typeof NonEmptyString.Type | typeof NumberFromString.Type
+
+// 3. Negative cases - NO DIAGNOSTIC
+type MixedPrimitive = typeof Circle.Type | null
+type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded
+const Lookalike = { Type: "lookalike" as const }
+type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type
+declare const AnyReceiver: any
+type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type
+type GenericAlias = typeof Circle.Type | typeof Square.Type
+declare type AmbientAlias = typeof Circle.Type | typeof Square.Type
+type SingleMember = typeof Circle.Type
+type NarrowedSchemaWithoutEncoded = {
+ readonly "~effect/Schema/Schema": true
+ readonly Type: string
+}
+declare const Narrowed: NarrowedSchemaWithoutEncoded
+type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type
+export const AlreadyComposed = Schema.Union([Circle, Square])
+export type AlreadyComposed = typeof AlreadyComposed.Type
+
+// 4. Diagnosed, but safety checks withhold code action
+export const CollidingValue = 42
+export type CollidingValue = typeof Circle.Type | typeof Square.Type
+
+export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type
+export const ForwardA = Schema.Struct({ a: Schema.String })
+export const ForwardB = Schema.Struct({ b: Schema.String })
+
+namespace Models {
+ export const SubItem = Schema.String
+}
+export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type
+
+let MutableSchema = Schema.Struct({ m: Schema.String })
+export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type
+
+export type CommentedUnion =
+ // interior comment
+ typeof Circle.Type | typeof Square.Type
+
+// 5. Existing aliased Schema import
+export const A = S.Struct({ a: S.String })
+export const B = S.Struct({ b: S.Number })
+export type AliasedUnion = typeof A.Type | typeof B.Type
+
+
+=== [D2] Fix 0: "Disable preferSchemaUnion for this line" ===
+skipped by default
+
+=== [D2] Fix 1: "Disable preferSchemaUnion for entire file" ===
+skipped by default
+
+=== [D2] Fix 2: "Compose Schema.Union and derive type" ===
+
+--- file:///.src/preferSchemaUnion.ts ---
+// @effect-diagnostics *:off
+// @effect-diagnostics preferSchemaUnion:warning
+import { Schema, Schema as S } from "effect"
+
+// 1. Two and three concrete schema leaves, including parenthesized unions
+export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number })
+
+export type Shape2 = typeof Circle.Type | typeof Square.Type
+export const Shape3 = Schema.Union([Circle, Square, Triangle]);
+export type Shape3 = typeof Shape3.Type
+
+// 2. Schema.Class, refinements and transformed codecs
+export class UserClass extends Schema.Class("UserClass")({
+ id: Schema.String,
+ name: Schema.String
+}) {}
+export const NonEmptyString = Schema.NonEmptyString
+export const NumberFromString = Schema.NumberFromString
+
+export type ValidEntities = typeof UserClass.Type | typeof NonEmptyString.Type | typeof NumberFromString.Type
+
+// 3. Negative cases - NO DIAGNOSTIC
+type MixedPrimitive = typeof Circle.Type | null
+type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded
+const Lookalike = { Type: "lookalike" as const }
+type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type
+declare const AnyReceiver: any
+type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type
+type GenericAlias = typeof Circle.Type | typeof Square.Type
+declare type AmbientAlias = typeof Circle.Type | typeof Square.Type
+type SingleMember = typeof Circle.Type
+type NarrowedSchemaWithoutEncoded = {
+ readonly "~effect/Schema/Schema": true
+ readonly Type: string
+}
+declare const Narrowed: NarrowedSchemaWithoutEncoded
+type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type
+export const AlreadyComposed = Schema.Union([Circle, Square])
+export type AlreadyComposed = typeof AlreadyComposed.Type
+
+// 4. Diagnosed, but safety checks withhold code action
+export const CollidingValue = 42
+export type CollidingValue = typeof Circle.Type | typeof Square.Type
+
+export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type
+export const ForwardA = Schema.Struct({ a: Schema.String })
+export const ForwardB = Schema.Struct({ b: Schema.String })
+
+namespace Models {
+ export const SubItem = Schema.String
+}
+export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type
+
+let MutableSchema = Schema.Struct({ m: Schema.String })
+export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type
+
+export type CommentedUnion =
+ // interior comment
+ typeof Circle.Type | typeof Square.Type
+
+// 5. Existing aliased Schema import
+export const A = S.Struct({ a: S.String })
+export const B = S.Struct({ b: S.Number })
+export type AliasedUnion = typeof A.Type | typeof B.Type
+
+
+=== [D3] Fix 0: "Disable preferSchemaUnion for this line" ===
+skipped by default
+
+=== [D3] Fix 1: "Disable preferSchemaUnion for entire file" ===
+skipped by default
+
+=== [D3] Fix 2: "Compose Schema.Union and derive type" ===
+
+--- file:///.src/preferSchemaUnion.ts ---
+// @effect-diagnostics *:off
+// @effect-diagnostics preferSchemaUnion:warning
+import { Schema, Schema as S } from "effect"
+
+// 1. Two and three concrete schema leaves, including parenthesized unions
+export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number })
+
+export type Shape2 = typeof Circle.Type | typeof Square.Type
+export type Shape3 = (typeof Circle.Type | typeof Square.Type) | typeof Triangle.Type
+
+// 2. Schema.Class, refinements and transformed codecs
+export class UserClass extends Schema.Class("UserClass")({
+ id: Schema.String,
+ name: Schema.String
+}) {}
+export const NonEmptyString = Schema.NonEmptyString
+export const NumberFromString = Schema.NumberFromString
+
+export const ValidEntities = Schema.Union([UserClass, NonEmptyString, NumberFromString]);
+export type ValidEntities = typeof ValidEntities.Type
+
+// 3. Negative cases - NO DIAGNOSTIC
+type MixedPrimitive = typeof Circle.Type | null
+type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded
+const Lookalike = { Type: "lookalike" as const }
+type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type
+declare const AnyReceiver: any
+type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type
+type GenericAlias = typeof Circle.Type | typeof Square.Type
+declare type AmbientAlias = typeof Circle.Type | typeof Square.Type
+type SingleMember = typeof Circle.Type
+type NarrowedSchemaWithoutEncoded = {
+ readonly "~effect/Schema/Schema": true
+ readonly Type: string
+}
+declare const Narrowed: NarrowedSchemaWithoutEncoded
+type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type
+export const AlreadyComposed = Schema.Union([Circle, Square])
+export type AlreadyComposed = typeof AlreadyComposed.Type
+
+// 4. Diagnosed, but safety checks withhold code action
+export const CollidingValue = 42
+export type CollidingValue = typeof Circle.Type | typeof Square.Type
+
+export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type
+export const ForwardA = Schema.Struct({ a: Schema.String })
+export const ForwardB = Schema.Struct({ b: Schema.String })
+
+namespace Models {
+ export const SubItem = Schema.String
+}
+export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type
+
+let MutableSchema = Schema.Struct({ m: Schema.String })
+export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type
+
+export type CommentedUnion =
+ // interior comment
+ typeof Circle.Type | typeof Square.Type
+
+// 5. Existing aliased Schema import
+export const A = S.Struct({ a: S.String })
+export const B = S.Struct({ b: S.Number })
+export type AliasedUnion = typeof A.Type | typeof B.Type
+
+
+=== [D12] Fix 0: "Disable preferSchemaUnion for this line" ===
+skipped by default
+
+=== [D12] Fix 1: "Disable preferSchemaUnion for entire file" ===
+skipped by default
+
+=== [D13] Fix 0: "Disable preferSchemaUnion for this line" ===
+skipped by default
+
+=== [D13] Fix 1: "Disable preferSchemaUnion for entire file" ===
+skipped by default
+
+=== [D14] Fix 0: "Disable preferSchemaUnion for this line" ===
+skipped by default
+
+=== [D14] Fix 1: "Disable preferSchemaUnion for entire file" ===
+skipped by default
+
+=== [D15] Fix 0: "Disable preferSchemaUnion for this line" ===
+skipped by default
+
+=== [D15] Fix 1: "Disable preferSchemaUnion for entire file" ===
+skipped by default
+
+=== [D16] Fix 0: "Disable preferSchemaUnion for this line" ===
+skipped by default
+
+=== [D16] Fix 1: "Disable preferSchemaUnion for entire file" ===
+skipped by default
+
+=== [D17] Fix 0: "Disable preferSchemaUnion for this line" ===
+skipped by default
+
+=== [D17] Fix 1: "Disable preferSchemaUnion for entire file" ===
+skipped by default
+
+=== [D17] Fix 2: "Compose Schema.Union and derive type" ===
+
+--- file:///.src/preferSchemaUnion.ts ---
+// @effect-diagnostics *:off
+// @effect-diagnostics preferSchemaUnion:warning
+import { Schema, Schema as S } from "effect"
+
+// 1. Two and three concrete schema leaves, including parenthesized unions
+export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number })
+
+export type Shape2 = typeof Circle.Type | typeof Square.Type
+export type Shape3 = (typeof Circle.Type | typeof Square.Type) | typeof Triangle.Type
+
+// 2. Schema.Class, refinements and transformed codecs
+export class UserClass extends Schema.Class("UserClass")({
+ id: Schema.String,
+ name: Schema.String
+}) {}
+export const NonEmptyString = Schema.NonEmptyString
+export const NumberFromString = Schema.NumberFromString
+
+export type ValidEntities = typeof UserClass.Type | typeof NonEmptyString.Type | typeof NumberFromString.Type
+
+// 3. Negative cases - NO DIAGNOSTIC
+type MixedPrimitive = typeof Circle.Type | null
+type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded
+const Lookalike = { Type: "lookalike" as const }
+type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type
+declare const AnyReceiver: any
+type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type
+type GenericAlias = typeof Circle.Type | typeof Square.Type
+declare type AmbientAlias = typeof Circle.Type | typeof Square.Type
+type SingleMember = typeof Circle.Type
+type NarrowedSchemaWithoutEncoded = {
+ readonly "~effect/Schema/Schema": true
+ readonly Type: string
+}
+declare const Narrowed: NarrowedSchemaWithoutEncoded
+type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type
+export const AlreadyComposed = Schema.Union([Circle, Square])
+export type AlreadyComposed = typeof AlreadyComposed.Type
+
+// 4. Diagnosed, but safety checks withhold code action
+export const CollidingValue = 42
+export type CollidingValue = typeof Circle.Type | typeof Square.Type
+
+export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type
+export const ForwardA = Schema.Struct({ a: Schema.String })
+export const ForwardB = Schema.Struct({ b: Schema.String })
+
+namespace Models {
+ export const SubItem = Schema.String
+}
+export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type
+
+let MutableSchema = Schema.Struct({ m: Schema.String })
+export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type
+
+export type CommentedUnion =
+ // interior comment
+ typeof Circle.Type | typeof Square.Type
+
+// 5. Existing aliased Schema import
+export const A = S.Struct({ a: S.String })
+export const B = S.Struct({ b: S.Number })
+export const AliasedUnion = Schema.Union([A, B]);
+export type AliasedUnion = typeof AliasedUnion.Type
+
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.errors.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.errors.txt
new file mode 100644
index 00000000..8c303570
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.errors.txt
@@ -0,0 +1,18 @@
+=== Metadata ===
+Effect version: 4.0.0
+
+/.src/preferSchemaUnion_preview.ts(8,21): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+
+
+==== /.src/preferSchemaUnion_preview.ts (1 errors) ====
+ // @effect-diagnostics *:off
+ // @effect-diagnostics preferSchemaUnion:warning
+ import { Schema } from "effect"
+
+ export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+ export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+
+ export type Shape = typeof Circle.Type | typeof Square.Type
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.preferSchemaUnion_preview.mermaid b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.preferSchemaUnion_preview.mermaid
new file mode 100644
index 00000000..0006ccc0
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.preferSchemaUnion_preview.mermaid
@@ -0,0 +1,25 @@
+flowchart TB
+ 0[/"type: #123; kind: Literal#lt;#quot;circle#quot;#gt;; radius: Number; #125;
node: #123; kind: Schema.Literal#40;#quot;circle#quot;#41;, radius: Schema.Number #125;"/]
+ 1[/"type: #quot;circle#quot;
node: #quot;circle#quot;"/]
+ 2["type: Literal#lt;#quot;circle#quot;#gt;
callee: Schema.Literal
args: #91;#93;"]
+ 3[/"type: #lt;L extends SchemaAST.LiteralValue#gt;#40;literal: L#41; =#gt; Literal#lt;L#gt;
node: Schema.Literal"/]
+ 4["type: Struct#lt;#123; readonly kind: Literal#lt;#quot;circle#quot;#gt;; readonly radius: Number; #125;#gt;
callee: Schema.Struct
args: #91;#93;"]
+ 5[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/]
+ 6[/"type: #123; kind: Literal#lt;#quot;square#quot;#gt;; side: Number; #125;
node: #123; kind: Schema.Literal#40;#quot;square#quot;#41;, side: Schema.Number #125;"/]
+ 7[/"type: #quot;square#quot;
node: #quot;square#quot;"/]
+ 8["type: Literal#lt;#quot;square#quot;#gt;
callee: Schema.Literal
args: #91;#93;"]
+ 9[/"type: #lt;L extends SchemaAST.LiteralValue#gt;#40;literal: L#41; =#gt; Literal#lt;L#gt;
node: Schema.Literal"/]
+ 10["type: Struct#lt;#123; readonly kind: Literal#lt;#quot;square#quot;#gt;; readonly side: Number; #125;#gt;
callee: Schema.Struct
args: #91;#93;"]
+ 11[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/]
+ 12[/"type:
node: Circle.Type"/]
+ 13[/"type:
node: Square.Type"/]
+ 1 -->|"kind: pipe"| 2
+ 3 -->|"kind: transformCallee"| 2
+ 2 -->|"kind: usedBy"| 0
+ 0 -->|"kind: pipe"| 4
+ 5 -->|"kind: transformCallee"| 4
+ 7 -->|"kind: pipe"| 8
+ 9 -->|"kind: transformCallee"| 8
+ 8 -->|"kind: usedBy"| 6
+ 6 -->|"kind: pipe"| 10
+ 11 -->|"kind: transformCallee"| 10
\ No newline at end of file
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.txt
new file mode 100644
index 00000000..0637ed6d
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.txt
@@ -0,0 +1 @@
+/.src/preferSchemaUnion_preview.ts -> preferSchemaUnion_preview.flows.preferSchemaUnion_preview.mermaid
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.layers.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.layers.txt
new file mode 100644
index 00000000..33b2870e
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.layers.txt
@@ -0,0 +1 @@
+==== /.src/preferSchemaUnion_preview.ts (0 layer exports) ====
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.pipings.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.pipings.txt
new file mode 100644
index 00000000..9e0b705c
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.pipings.txt
@@ -0,0 +1,57 @@
+==== /.src/preferSchemaUnion_preview.ts (4 flows) ====
+
+=== Piping Flow ===
+Location: 5:22 - 5:95
+Node: Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+Node Kind: KindCallExpression
+
+Subject: { kind: Schema.Literal("circle"), radius: Schema.Number }
+Subject Type: { kind: Literal<"circle">; radius: Number; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Struct
+ args: (constant)
+ outType: Struct<{ readonly kind: Literal<"circle">; readonly radius: Number; }>
+
+=== Piping Flow ===
+Location: 5:44 - 5:69
+Node: Schema.Literal("circle")
+Node Kind: KindCallExpression
+
+Subject: "circle"
+Subject Type: "circle"
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Literal
+ args: (constant)
+ outType: Literal<"circle">
+
+=== Piping Flow ===
+Location: 6:22 - 6:93
+Node: Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+Node Kind: KindCallExpression
+
+Subject: { kind: Schema.Literal("square"), side: Schema.Number }
+Subject Type: { kind: Literal<"square">; side: Number; }
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Struct
+ args: (constant)
+ outType: Struct<{ readonly kind: Literal<"square">; readonly side: Number; }>
+
+=== Piping Flow ===
+Location: 6:44 - 6:69
+Node: Schema.Literal("square")
+Node Kind: KindCallExpression
+
+Subject: "square"
+Subject Type: "square"
+
+Transformations (1):
+ [0] kind: call
+ callee: Schema.Literal
+ args: (constant)
+ outType: Literal<"square">
diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.quickfixes.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.quickfixes.txt
new file mode 100644
index 00000000..6221cf00
--- /dev/null
+++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.quickfixes.txt
@@ -0,0 +1,28 @@
+=== Quick Fix Inventory ===
+
+[D1] (8:21-8:60) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)
+ Fix 0: "Disable preferSchemaUnion for this line"
+ Fix 1: "Disable preferSchemaUnion for entire file"
+ Fix 2: "Compose Schema.Union and derive type"
+
+=== Quick Fix Application Results ===
+
+=== [D1] Fix 0: "Disable preferSchemaUnion for this line" ===
+skipped by default
+
+=== [D1] Fix 1: "Disable preferSchemaUnion for entire file" ===
+skipped by default
+
+=== [D1] Fix 2: "Compose Schema.Union and derive type" ===
+
+--- file:///.src/preferSchemaUnion_preview.ts ---
+// @effect-diagnostics *:off
+// @effect-diagnostics preferSchemaUnion:warning
+import { Schema } from "effect"
+
+export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+
+export const Shape = Schema.Union([Circle, Square]);
+export type Shape = typeof Shape.Type
+
diff --git a/testdata/tests/effect-v3/preferSchemaUnion.ts b/testdata/tests/effect-v3/preferSchemaUnion.ts
new file mode 100644
index 00000000..e06162e4
--- /dev/null
+++ b/testdata/tests/effect-v3/preferSchemaUnion.ts
@@ -0,0 +1,9 @@
+// @effect-v3
+// @effect-diagnostics *:off
+// @effect-diagnostics preferSchemaUnion:warning
+import { Schema } from "effect"
+
+const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+
+export type Shape = typeof Circle.Type | typeof Square.Type
diff --git a/testdata/tests/effect-v4/preferSchemaUnion.ts b/testdata/tests/effect-v4/preferSchemaUnion.ts
new file mode 100644
index 00000000..cc57878a
--- /dev/null
+++ b/testdata/tests/effect-v4/preferSchemaUnion.ts
@@ -0,0 +1,65 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics preferSchemaUnion:warning
+import { Schema, Schema as S } from "effect"
+
+// 1. Two and three concrete schema leaves, including parenthesized unions
+export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number })
+
+export type Shape2 = typeof Circle.Type | typeof Square.Type
+export type Shape3 = (typeof Circle.Type | typeof Square.Type) | typeof Triangle.Type
+
+// 2. Schema.Class, refinements and transformed codecs
+export class UserClass extends Schema.Class("UserClass")({
+ id: Schema.String,
+ name: Schema.String
+}) {}
+export const NonEmptyString = Schema.NonEmptyString
+export const NumberFromString = Schema.NumberFromString
+
+export type ValidEntities = typeof UserClass.Type | typeof NonEmptyString.Type | typeof NumberFromString.Type
+
+// 3. Negative cases - NO DIAGNOSTIC
+type MixedPrimitive = typeof Circle.Type | null
+type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded
+const Lookalike = { Type: "lookalike" as const }
+type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type
+declare const AnyReceiver: any
+type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type
+type GenericAlias = typeof Circle.Type | typeof Square.Type
+declare type AmbientAlias = typeof Circle.Type | typeof Square.Type
+type SingleMember = typeof Circle.Type
+type NarrowedSchemaWithoutEncoded = {
+ readonly "~effect/Schema/Schema": true
+ readonly Type: string
+}
+declare const Narrowed: NarrowedSchemaWithoutEncoded
+type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type
+export const AlreadyComposed = Schema.Union([Circle, Square])
+export type AlreadyComposed = typeof AlreadyComposed.Type
+
+// 4. Diagnosed, but safety checks withhold code action
+export const CollidingValue = 42
+export type CollidingValue = typeof Circle.Type | typeof Square.Type
+
+export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type
+export const ForwardA = Schema.Struct({ a: Schema.String })
+export const ForwardB = Schema.Struct({ b: Schema.String })
+
+namespace Models {
+ export const SubItem = Schema.String
+}
+export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type
+
+let MutableSchema = Schema.Struct({ m: Schema.String })
+export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type
+
+export type CommentedUnion =
+ // interior comment
+ typeof Circle.Type | typeof Square.Type
+
+// 5. Existing aliased Schema import
+export const A = S.Struct({ a: S.String })
+export const B = S.Struct({ b: S.Number })
+export type AliasedUnion = typeof A.Type | typeof B.Type
diff --git a/testdata/tests/effect-v4/preferSchemaUnion_preview.ts b/testdata/tests/effect-v4/preferSchemaUnion_preview.ts
new file mode 100644
index 00000000..58ed4d60
--- /dev/null
+++ b/testdata/tests/effect-v4/preferSchemaUnion_preview.ts
@@ -0,0 +1,8 @@
+// @effect-diagnostics *:off
+// @effect-diagnostics preferSchemaUnion:warning
+import { Schema } from "effect"
+
+export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number })
+export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number })
+
+export type Shape = typeof Circle.Type | typeof Square.Type