swc-project/swc · error

enum member types must be consistent: {:?}

Error message

enum member types must be consistent: {:?}

What it means

Decode's derive folds all non-unknown variants into one EnumType (Unit/One/Struct) and requires the mix to stay uniform. Named-field variants cannot coexist with single-field tuple ('newtype') variants — the (Struct, One) combination falls into the catch-all and panics; note that Unit+One is internally upgraded to Struct, so Unit+One+One also ends in this panic. The {:?} payload prints the conflicting pair (e.g. (Struct, One)) so you can see which shapes collided.

Source

Thrown at crates/ast_node/src/encoding/decode.rs:99

                None,
                |mut sum, next| {
                    let ty = match &next.fields {
                        syn::Fields::Named(_) => EnumType::Struct,
                        syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => EnumType::One,
                        syn::Fields::Unit => EnumType::Unit,
                        syn::Fields::Unnamed(_) => {
                            panic!("more than 1 unnamed member field are not allowed")
                        }
                    };
                    match (*sum.get_or_insert(ty), ty) {
                        (EnumType::Struct, EnumType::Struct)
                        | (EnumType::Struct, EnumType::Unit)
                        | (EnumType::Unit, EnumType::Unit)
                        | (EnumType::One, EnumType::One) => (),
                        (EnumType::Unit, EnumType::One)
                        | (EnumType::One, EnumType::Unit)
                        | (_, EnumType::Struct) => sum = Some(EnumType::Struct),
                        _ => panic!("enum member types must be consistent: {:?}", (sum, ty)),
                    }
                    sum
                },
            );
            let enum_type = enum_type.expect("enum cannot be empty");
            let mut iter = data.variants.iter().peekable();

            let unknown_arm: Option<syn::Arm> = iter.next_if(|variant| is_unknown(&variant.attrs))
                .map(|unknown| {
                    let name = &unknown.ident;
                    assert!(
                        unknown.discriminant.is_none(),
                        "unknown member is not allowed custom discriminant"
                    );
                    assert!(
                        is_with(&unknown.attrs).is_none(),
                        "unknown member is not allowed with type"
                    );

View on GitHub (pinned to 5176682b65)

Solutions

  1. Make all variants the same shape — usually convert newtype variants `B(u32)` to named ones `B { value: u32 }`.
  2. Alternatively keep newtypes but wrap named payloads in a struct so every variant is single-field.
  3. Read the (X, Y) pair in the panic message to find which two variant shapes disagree.

Example fix

// before
#[derive(Encode, Decode)]
enum Value {
    Num(f64),                 // One
    Obj { props: Vec<Prop> }, // Struct — inconsistent
}

// after — uniform named-struct variants
#[derive(Encode, Decode)]
enum Value {
    Num { value: f64 },
    Obj { props: Vec<Prop> },
}
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: An enum mixing `A { x: u32 }` (Struct) with `B(u32)` (One) under derive(Decode)/#[ast_node]; or an enum with Unit plus multiple One variants, which folds to Struct and then clashes with a later One.

Common situations: Growing an AST enum incrementally — starting with newtype variants and later adding a named-variant node; merging vendor branches whose variants use different styles.

Related errors


AI-assisted analysis of swc-project/swc@5176682b65 (2026-08-17). Data as JSON: /api/errors/75a37544dd01c5f6. Report an issue: GitHub.