diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs
index d745f325f9e..b0be69a2137 100644
--- a/crates/backend/src/ast.rs
+++ b/crates/backend/src/ast.rs
@@ -73,6 +73,32 @@ impl Program {
     }
 }
 
+/// The name of a JS method, field, or property.
+///
+/// JavaScript, broadly, has 2 ways to access properties on objects:
+///
+/// 1. String properties. E.g. `obj.foo` or `obj['foo']`.
+/// 2. Symbol properties. E.g. `obj[Symbol.iterator]`.
+///
+/// String properties are the most common, and are represented by the
+/// `Identifier` variant. This makes code gen easier, because we allowed to
+/// use the `.` operator in JS to access the property.
+///
+/// Symbol properties are less common but no less important. Many JS protocols
+/// (like iterators) are defined by well-known symbols. Furthermore, this also
+/// supports custom symbols created by `Symbol.for(key)`.
+///
+/// Note that symbols are only allowed for properties, fields, and methods.
+/// Free functions, enums, types, and classes cannot be named with symbols.
+#[cfg_attr(feature = "extra-traits", derive(Debug))]
+#[derive(Clone)]
+pub enum Name {
+    /// A valid JS identifier.
+    Identifier(String),
+    /// The name of a well-known symbol. E.g. `iterator` for `Symbol.iterator`.
+    Symbol(String),
+}
+
 /// An abstract syntax tree representing a link to a module in Rust.
 /// In contrast to Program, LinkToModule must expand to an expression.
 /// linked_modules of the inner Program must contain exactly one element
@@ -248,9 +274,9 @@ pub enum OperationKind {
     /// A standard method, nothing special
     Regular,
     /// A method for getting the value of the provided Ident or String
-    Getter(Option<String>),
+    Getter(Option<Name>),
     /// A method for setting the value of the provided Ident or String
-    Setter(Option<String>),
+    Setter(Option<Name>),
     /// A dynamically intercepted getter
     IndexingGetter,
     /// A dynamically intercepted setter
@@ -358,11 +384,9 @@ pub struct StringEnum {
 #[derive(Clone)]
 pub struct Function {
     /// The name of the function
-    pub name: String,
+    pub name: Name,
     /// The span of the function's name in Rust code
     pub name_span: Span,
-    /// Whether the function has a js_name attribute
-    pub renamed_via_js_name: bool,
     /// The arguments to the function
     pub arguments: Vec<syn::PatType>,
     /// The return type of the function, if provided
@@ -410,7 +434,7 @@ pub struct StructField {
     /// The name of the field in Rust code
     pub rust_name: syn::Member,
     /// The name of the field in JS code
-    pub js_name: String,
+    pub js_name: Name,
     /// The name of the struct this field is part of
     pub struct_name: Ident,
     /// Whether this value is read-only to JS
@@ -513,7 +537,7 @@ impl Export {
             generated_name.push_str(class);
         }
         generated_name.push('_');
-        generated_name.push_str(&self.function.name.to_string());
+        generated_name.push_str(&self.function.name.as_ref().disambiguated_name());
         Ident::new(&generated_name, Span::call_site())
     }
 
@@ -521,10 +545,10 @@ impl Export {
     /// ABI form of its arguments and converts them back into their normal,
     /// "high level" form before calling the actual function.
     pub(crate) fn export_name(&self) -> String {
-        let fn_name = self.function.name.to_string();
+        let fn_name = self.function.name.as_ref();
         match &self.js_class {
-            Some(class) => shared::struct_function_export_name(class, &fn_name),
-            None => shared::free_function_export_name(&fn_name),
+            Some(class) => shared::struct_function_export_name(class, fn_name),
+            None => shared::free_function_export_name(fn_name),
         }
     }
 }
@@ -542,26 +566,41 @@ impl ImportKind {
     }
 }
 
+impl Name {
+    /// Turn this into a name ref to take advantage of shared logic.
+    pub fn as_ref(&self) -> shared::NameRef<'_> {
+        match self {
+            Name::Identifier(s) => shared::NameRef::Identifier(s),
+            Name::Symbol(s) => shared::NameRef::Symbol(s),
+        }
+    }
+}
+
 impl Function {
     /// If the rust object has a `fn xxx(&self) -> MyType` method, get the name for a getter in
     /// javascript (in this case `xxx`, so you can write `val = obj.xxx`)
-    pub fn infer_getter_property(&self) -> &str {
+    pub fn infer_getter_property(&self) -> &Name {
         &self.name
     }
 
     /// If the rust object has a `fn set_xxx(&mut self, MyType)` style method, get the name
     /// for a setter in javascript (in this case `xxx`, so you can write `obj.xxx = val`)
-    pub fn infer_setter_property(&self) -> Result<String, Diagnostic> {
-        let name = self.name.to_string();
-
-        // Otherwise we infer names based on the Rust function name.
-        if !name.starts_with("set_") {
-            bail_span!(
-                syn::token::Pub(self.name_span),
-                "setters must start with `set_`, found: {}",
-                name,
-            );
+    pub fn infer_setter_property(&self) -> Result<Name, Diagnostic> {
+        match &self.name {
+            Name::Identifier(ref name) => {
+                let name = name.to_string();
+
+                // Otherwise we infer names based on the Rust function name.
+                if !name.starts_with("set_") {
+                    bail_span!(
+                        syn::token::Pub(self.name_span),
+                        "setters must start with `set_`, found: {}",
+                        name,
+                    );
+                }
+                Ok(Name::Identifier(name[4..].to_string()))
+            }
+            Name::Symbol(_) => Ok(self.name.clone()),
         }
-        Ok(name[4..].to_string())
     }
 }
diff --git a/crates/backend/src/encode.rs b/crates/backend/src/encode.rs
index bc82ba5c336..adba5346376 100644
--- a/crates/backend/src/encode.rs
+++ b/crates/backend/src/encode.rs
@@ -214,6 +214,13 @@ fn shared_export<'a>(
     })
 }
 
+fn shared_name<'a>(func: &ast::Name, intern: &'a Interner) -> Name<'a> {
+    match func {
+        ast::Name::Identifier(x) => Name::Identifier(intern.intern_str(x)),
+        ast::Name::Symbol(x) => Name::Symbol(intern.intern_str(x)),
+    }
+}
+
 fn shared_function<'a>(func: &'a ast::Function, _intern: &'a Interner) -> Function<'a> {
     let arg_names = func
         .arguments
@@ -229,7 +236,7 @@ fn shared_function<'a>(func: &'a ast::Function, _intern: &'a Interner) -> Functi
     Function {
         arg_names,
         asyncness: func.r#async,
-        name: &func.name,
+        name: shared_name(&func.name, _intern),
         generate_typescript: func.generate_typescript,
         generate_jsdoc: func.generate_jsdoc,
         variadic: func.variadic,
@@ -382,9 +389,9 @@ fn shared_struct<'a>(s: &'a ast::Struct, intern: &'a Interner) -> Struct<'a> {
     }
 }
 
-fn shared_struct_field<'a>(s: &'a ast::StructField, _intern: &'a Interner) -> StructField<'a> {
+fn shared_struct_field<'a>(s: &'a ast::StructField, intern: &'a Interner) -> StructField<'a> {
     StructField {
-        name: &s.js_name,
+        name: shared_name(&s.js_name, intern),
         readonly: s.readonly,
         comments: s.comments.iter().map(|s| &**s).collect(),
         generate_typescript: s.generate_typescript,
@@ -594,16 +601,19 @@ fn from_ast_method_kind<'a>(
             let is_static = *is_static;
             let kind = match kind {
                 ast::OperationKind::Getter(g) => {
-                    let g = g.as_ref().map(|g| intern.intern_str(g));
-                    OperationKind::Getter(g.unwrap_or_else(|| function.infer_getter_property()))
+                    let g = g
+                        .as_ref()
+                        .unwrap_or_else(|| function.infer_getter_property());
+                    OperationKind::Getter(shared_name(g, intern))
                 }
                 ast::OperationKind::Regular => OperationKind::Regular,
                 ast::OperationKind::Setter(s) => {
-                    let s = s.as_ref().map(|s| intern.intern_str(s));
-                    OperationKind::Setter(match s {
-                        Some(s) => s,
-                        None => intern.intern_str(&function.infer_setter_property()?),
-                    })
+                    let s = if let Some(s) = s {
+                        shared_name(s, intern)
+                    } else {
+                        shared_name(&function.infer_setter_property()?, intern)
+                    };
+                    OperationKind::Setter(s)
                 }
                 ast::OperationKind::IndexingGetter => OperationKind::IndexingGetter,
                 ast::OperationKind::IndexingSetter => OperationKind::IndexingSetter,
diff --git a/crates/cli-support/src/decode.rs b/crates/cli-support/src/decode.rs
index 4264939f5c4..da9dce12c5a 100644
--- a/crates/cli-support/src/decode.rs
+++ b/crates/cli-support/src/decode.rs
@@ -173,3 +173,19 @@ macro_rules! decode_api {
 }
 
 wasm_bindgen_shared::shared_api!(decode_api);
+
+impl Name<'_> {
+    pub fn as_ref(&self) -> wasm_bindgen_shared::NameRef<'_> {
+        match self {
+            Name::Identifier(s) => wasm_bindgen_shared::NameRef::Identifier(s),
+            Name::Symbol(s) => wasm_bindgen_shared::NameRef::Symbol(s),
+        }
+    }
+
+    pub fn to_aux(&self) -> crate::wit::AuxName {
+        match self {
+            Name::Identifier(s) => crate::wit::AuxName::Identifier(s.to_string()),
+            Name::Symbol(s) => crate::wit::AuxName::Symbol(s.to_string()),
+        }
+    }
+}
diff --git a/crates/cli-support/src/js/mod.rs b/crates/cli-support/src/js/mod.rs
index 10658f37f3a..ef94e148c32 100644
--- a/crates/cli-support/src/js/mod.rs
+++ b/crates/cli-support/src/js/mod.rs
@@ -1,8 +1,8 @@
 use crate::descriptor::VectorKind;
 use crate::intrinsic::Intrinsic;
 use crate::wit::{
-    Adapter, AdapterId, AdapterJsImportKind, AdapterType, AuxExportedMethodKind, AuxReceiverKind,
-    AuxStringEnum, AuxValue,
+    Adapter, AdapterId, AdapterJsImportKind, AdapterType, AuxExportedMethodKind, AuxName,
+    AuxReceiverKind, AuxStringEnum, AuxValue,
 };
 use crate::wit::{AdapterKind, Instruction, InstructionData};
 use crate::wit::{AuxEnum, AuxExport, AuxExportKind, AuxImport, AuxStruct};
@@ -97,7 +97,7 @@ pub struct ExportedClass {
     readable_properties: Vec<String>,
     /// Map from field name to type as a string, docs plus whether it has a setter,
     /// whether it's optional and whether it's static.
-    typescript_fields: HashMap<String, (String, String, bool, bool, bool)>,
+    typescript_fields: HashMap<AuxName, (String, String, bool, bool, bool)>,
 }
 
 const INITIAL_HEAP_VALUES: &[&str] = &["undefined", "null", "true", "false"];
@@ -1142,7 +1142,7 @@ __wbg_set_wasm(wasm);"
             if !has_setter {
                 ts_dst.push_str("readonly ");
             }
-            ts_dst.push_str(name);
+            ts_dst.push_str(&property_definition(name));
             if *is_optional {
                 ts_dst.push_str("?: ");
             } else {
@@ -2365,8 +2365,7 @@ __wbg_set_wasm(wasm);"
         if let Some(name) = self.imported_names.get(&import.name) {
             let mut name = name.clone();
             for field in import.fields.iter() {
-                name.push('.');
-                name.push_str(field);
+                name.push_str(&property_accessor(field));
             }
             return Ok(name.clone());
         }
@@ -2436,8 +2435,7 @@ __wbg_set_wasm(wasm);"
 
         // After we've got an actual name handle field projections
         for field in import.fields.iter() {
-            name.push('.');
-            name.push_str(field);
+            name.push_str(&property_accessor(field));
         }
         Ok(name)
     }
@@ -2723,7 +2721,14 @@ __wbg_set_wasm(wasm);"
                         }
 
                         exported.has_constructor = true;
-                        exported.push("constructor", "", &js_docs, &code, &ts_docs, ts_sig);
+                        exported.push(
+                            &AuxName::Identifier("constructor".to_string()),
+                            "",
+                            &js_docs,
+                            &code,
+                            &ts_docs,
+                            ts_sig,
+                        );
                     }
                     AuxExportKind::Method {
                         class,
@@ -2755,7 +2760,9 @@ __wbg_set_wasm(wasm);"
                                     );
                                 }
                                 // Add the getter to the list of readable fields (used to generate `toJSON`)
-                                exported.readable_properties.push(name.clone());
+                                if let AuxName::Identifier(name) = name {
+                                    exported.readable_properties.push(name.clone());
+                                }
                                 // Ignore the raw signature.
                                 None
                             }
@@ -3041,10 +3048,10 @@ __wbg_set_wasm(wasm);"
                     Ok(format!("new {}({})", js, variadic_args(args)?))
                 }
                 AdapterJsImportKind::Method => {
-                    let descriptor = |anchor: &str, extra: &str, field: &str, which: &str| {
+                    let descriptor = |anchor: &str, extra: &str, field: &AuxName, which: &str| {
                         format!(
-                            "GetOwnOrInheritedPropertyDescriptor({}{}, '{}').{}",
-                            anchor, extra, field, which
+                            "GetOwnOrInheritedPropertyDescriptor({anchor}{extra}, {field}).{which}",
+                            field = property_key(field),
                         )
                     };
                     let js = match val {
@@ -4234,10 +4241,10 @@ fn check_duplicated_getter_and_setter_names(
 ) -> Result<(), Error> {
     fn verify_exports(
         first_class: &str,
-        first_field: &str,
+        first_field: &AuxName,
         first_receiver: &AuxReceiverKind,
         second_class: &str,
-        second_field: &str,
+        second_field: &AuxName,
         second_receiver: &AuxReceiverKind,
     ) -> Result<(), Error> {
         let both_are_in_the_same_class = first_class == second_class;
@@ -4246,7 +4253,8 @@ fn check_duplicated_getter_and_setter_names(
         if both_are_in_the_same_class && both_are_referencing_the_same_field {
             bail!(format!(
                 "There can be only one getter/setter definition for `{}` in `{}`",
-                first_field, first_class
+                first_field.as_ref().debug_name(),
+                first_class
             ));
         }
         Ok(())
@@ -4388,27 +4396,67 @@ fn is_valid_ident(name: &str) -> bool {
 /// In most cases, this is `.<name>`, generating accesses like `foo.bar`.
 /// However, if `name` is not a valid JavaScript identifier, it becomes
 /// `["<name>"]` instead, creating accesses like `foo["kebab-case"]`.
-fn property_accessor(name: &str) -> String {
-    if is_valid_ident(name) {
-        format!(".{name}")
-    } else {
-        format!("[\"{}\"]", name.escape_default())
+///
+/// Symbols are also supported, and will be accessed as `[Symbol.<name>]`.
+/// E.g. `foo[Symbol.iterator]`.
+fn property_accessor(name: &AuxName) -> String {
+    match name {
+        AuxName::Identifier(name) => {
+            if is_valid_ident(name) {
+                format!(".{name}")
+            } else {
+                format!("[\"{}\"]", name.escape_default())
+            }
+        }
+        AuxName::Symbol(name) => format!("[Symbol.{name}]"),
+    }
+}
+
+/// Similar to `property_accessor`, but for property definitions.
+///
+/// E.g. for a property named `foo`, this will return `foo`. For string-like
+/// names, it will return `["foo-bar"]`. For symbols, it will return
+/// `[Symbol.foo]`.
+fn property_definition(name: &AuxName) -> String {
+    match name {
+        AuxName::Identifier(name) => {
+            if is_valid_ident(name) {
+                name.to_string()
+            } else {
+                format!("[\"{}\"]", name.escape_default())
+            }
+        }
+        AuxName::Symbol(name) => format!("[Symbol.{name}]"),
+    }
+}
+
+/// Similar the runtime string or symbol value of a property of the given name.
+///
+/// E.g. for a property named `foo`, this will return `"foo"`. For string-like
+/// names, it will return `"foo-bar"`. For symbols, it will return
+/// `Symbol.foo`.
+fn property_key(name: &AuxName) -> String {
+    match name {
+        AuxName::Identifier(name) => format!("\"{}\"", name.escape_default()),
+        AuxName::Symbol(name) => format!("Symbol.{name}"),
     }
 }
 
 impl ExportedClass {
     fn push(
         &mut self,
-        function_name: &str,
+        function_name: &AuxName,
         function_prefix: &str,
         js_docs: &str,
         js: &str,
         ts_docs: &str,
         ts: Option<&str>,
     ) {
+        let name_def = property_definition(function_name);
+
         self.contents.push_str(js_docs);
         self.contents.push_str(function_prefix);
-        self.contents.push_str(function_name);
+        self.contents.push_str(&name_def);
         self.contents.push_str(js);
         self.contents.push('\n');
         if let Some(ts) = ts {
@@ -4421,7 +4469,7 @@ impl ExportedClass {
             }
             self.typescript.push_str("  ");
             self.typescript.push_str(function_prefix);
-            self.typescript.push_str(function_name);
+            self.typescript.push_str(&name_def);
             self.typescript.push_str(ts);
             self.typescript.push_str(";\n");
         }
@@ -4431,13 +4479,13 @@ impl ExportedClass {
     fn push_accessor_ts(
         &mut self,
         docs: &str,
-        field: &str,
+        field: &AuxName,
         ty: &str,
         is_setter: bool,
         is_static: bool,
     ) -> &mut bool {
         let (ty_dst, accessor_docs, has_setter, is_optional, is_static_dst) =
-            self.typescript_fields.entry(field.to_string()).or_default();
+            self.typescript_fields.entry(field.clone()).or_default();
 
         *ty_dst = ty.to_string();
         // Deterministic output: always use the getter's docs if available
diff --git a/crates/cli-support/src/wit/mod.rs b/crates/cli-support/src/wit/mod.rs
index 3e3e355c366..4600bbe6e0a 100644
--- a/crates/cli-support/src/wit/mod.rs
+++ b/crates/cli-support/src/wit/mod.rs
@@ -8,7 +8,7 @@ use std::collections::{BTreeSet, HashMap};
 use std::str;
 use walrus::MemoryId;
 use walrus::{ExportId, FunctionId, ImportId, Module};
-use wasm_bindgen_shared::struct_function_export_name;
+use wasm_bindgen_shared::{free_function_export_name, struct_function_export_name, NameRef};
 use wasm_bindgen_threads_xform::ThreadCount;
 
 mod incoming;
@@ -473,8 +473,8 @@ impl<'a> Context<'a> {
 
     fn export(&mut self, export: decode::Export<'_>) -> Result<(), Error> {
         let wasm_name = match &export.class {
-            Some(class) => struct_function_export_name(class, export.function.name),
-            None => export.function.name.to_string(),
+            Some(class) => struct_function_export_name(class, export.function.name.as_ref()),
+            None => free_function_export_name(export.function.name.as_ref()),
         };
         let mut descriptor = match self.descriptors.remove(&wasm_name) {
             None => return Ok(()),
@@ -507,7 +507,7 @@ impl<'a> Context<'a> {
 
                         AuxExportKind::Method {
                             class,
-                            name: name.to_owned(),
+                            name: name.to_aux(),
                             receiver: if op.is_static {
                                 AuxReceiverKind::None
                             } else if export.consumed {
@@ -520,7 +520,9 @@ impl<'a> Context<'a> {
                     }
                 }
             }
-            None => AuxExportKind::Function(export.function.name.to_string()),
+            None => {
+                AuxExportKind::Function(export.function.name.as_ref().free_function().to_string())
+            }
         };
 
         let id = self.export_adapter(export_id, descriptor)?;
@@ -610,7 +612,7 @@ impl<'a> Context<'a> {
         // to the WebAssembly instance.
         let (id, import) = match method {
             Some(data) => {
-                let class = self.determine_import(import, data.class)?;
+                let class = self.determine_import(import, NameRef::Identifier(data.class))?;
                 match &data.kind {
                     // NB: `structural` is ignored for constructors since the
                     // js type isn't expected to change anyway.
@@ -639,7 +641,7 @@ impl<'a> Context<'a> {
             // expected that the binding isn't changing anyway.
             None => {
                 let id = self.import_adapter(import_id, descriptor, AdapterJsImportKind::Normal)?;
-                let name = self.determine_import(import, function.name)?;
+                let name = self.determine_import(import, function.name.as_ref())?;
                 (id, AuxImport::Value(AuxValue::Bare(name)))
             }
         };
@@ -682,21 +684,20 @@ impl<'a> Context<'a> {
         structural: bool,
         op: &decode::Operation<'_>,
     ) -> Result<(AuxImport, bool), Error> {
-        match op.kind {
+        match &op.kind {
             decode::OperationKind::Regular => {
                 if op.is_static {
                     Ok((
-                        AuxImport::ValueWithThis(class, function.name.to_string()),
+                        AuxImport::ValueWithThis(class, function.name.to_aux()),
                         false,
                     ))
                 } else if structural {
-                    Ok((
-                        AuxImport::StructuralMethod(function.name.to_string()),
-                        false,
-                    ))
+                    Ok((AuxImport::StructuralMethod(function.name.to_aux()), false))
                 } else {
-                    class.fields.push("prototype".to_string());
-                    class.fields.push(function.name.to_string());
+                    class
+                        .fields
+                        .push(AuxName::Identifier("prototype".to_string()));
+                    class.fields.push(function.name.to_aux());
                     Ok((AuxImport::Value(AuxValue::Bare(class)), true))
                 }
             }
@@ -705,17 +706,17 @@ impl<'a> Context<'a> {
                 if structural {
                     if op.is_static {
                         Ok((
-                            AuxImport::StructuralClassGetter(class, field.to_string()),
+                            AuxImport::StructuralClassGetter(class, field.to_aux()),
                             false,
                         ))
                     } else {
-                        Ok((AuxImport::StructuralGetter(field.to_string()), false))
+                        Ok((AuxImport::StructuralGetter(field.to_aux()), false))
                     }
                 } else {
                     let val = if op.is_static {
-                        AuxValue::ClassGetter(class, field.to_string())
+                        AuxValue::ClassGetter(class, field.to_aux())
                     } else {
-                        AuxValue::Getter(class, field.to_string())
+                        AuxValue::Getter(class, field.to_aux())
                     };
                     Ok((AuxImport::Value(val), true))
                 }
@@ -725,17 +726,17 @@ impl<'a> Context<'a> {
                 if structural {
                     if op.is_static {
                         Ok((
-                            AuxImport::StructuralClassSetter(class, field.to_string()),
+                            AuxImport::StructuralClassSetter(class, field.to_aux()),
                             false,
                         ))
                     } else {
-                        Ok((AuxImport::StructuralSetter(field.to_string()), false))
+                        Ok((AuxImport::StructuralSetter(field.to_aux()), false))
                     }
                 } else {
                     let val = if op.is_static {
-                        AuxValue::ClassSetter(class, field.to_string())
+                        AuxValue::ClassSetter(class, field.to_aux())
                     } else {
-                        AuxValue::Setter(class, field.to_string())
+                        AuxValue::Setter(class, field.to_aux())
                     };
                     Ok((AuxImport::Value(val), true))
                 }
@@ -805,7 +806,7 @@ impl<'a> Context<'a> {
 
         // And then save off that this function is is an instanceof shim for an
         // imported item.
-        let import = self.determine_import(import, static_.name)?;
+        let import = self.determine_import(import, NameRef::Identifier(static_.name))?;
         self.aux.import_map.insert(id, AuxImport::Static(import));
         Ok(())
     }
@@ -860,7 +861,7 @@ impl<'a> Context<'a> {
 
         // And then save off that this function is is an instanceof shim for an
         // imported item.
-        let import = self.determine_import(import, type_.name)?;
+        let import = self.determine_import(import, NameRef::Identifier(type_.name))?;
         self.aux
             .import_map
             .insert(id, AuxImport::Instanceof(import));
@@ -923,12 +924,13 @@ impl<'a> Context<'a> {
 
     fn struct_(&mut self, struct_: decode::Struct<'_>) -> Result<(), Error> {
         for field in struct_.fields {
-            let getter = wasm_bindgen_shared::struct_field_get(struct_.name, field.name);
-            let setter = wasm_bindgen_shared::struct_field_set(struct_.name, field.name);
+            let getter = wasm_bindgen_shared::struct_field_get(struct_.name, field.name.as_ref());
+            let setter = wasm_bindgen_shared::struct_field_set(struct_.name, field.name.as_ref());
             let descriptor = match self.descriptors.remove(&getter) {
                 None => continue,
                 Some(d) => d,
             };
+            let debug_name = field.name.as_ref().debug_name();
 
             // Register a webidl transformation for the getter
             let (getter_id, _) = self.function_exports[&getter];
@@ -942,13 +944,13 @@ impl<'a> Context<'a> {
             self.aux.export_map.insert(
                 getter_id,
                 AuxExport {
-                    debug_name: format!("getter for `{}::{}`", struct_.name, field.name),
+                    debug_name: format!("getter for `{}::{}`", struct_.name, debug_name),
                     arg_names: None,
                     asyncness: false,
                     comments: concatenate_comments(&field.comments),
                     kind: AuxExportKind::Method {
                         class: struct_.name.to_string(),
-                        name: field.name.to_string(),
+                        name: field.name.to_aux(),
                         receiver: AuxReceiverKind::Borrowed,
                         kind: AuxExportedMethodKind::Getter,
                     },
@@ -974,13 +976,13 @@ impl<'a> Context<'a> {
             self.aux.export_map.insert(
                 setter_id,
                 AuxExport {
-                    debug_name: format!("setter for `{}::{}`", struct_.name, field.name),
+                    debug_name: format!("setter for `{}::{}`", struct_.name, debug_name),
                     arg_names: None,
                     asyncness: false,
                     comments: concatenate_comments(&field.comments),
                     kind: AuxExportKind::Method {
                         class: struct_.name.to_string(),
-                        name: field.name.to_string(),
+                        name: field.name.to_aux(),
                         receiver: AuxReceiverKind::Borrowed,
                         kind: AuxExportedMethodKind::Setter,
                     },
@@ -1038,57 +1040,66 @@ impl<'a> Context<'a> {
         Ok(())
     }
 
-    fn determine_import(&self, import: &decode::Import<'_>, item: &str) -> Result<JsImport, Error> {
+    fn determine_import(
+        &self,
+        import: &decode::Import<'_>,
+        item: NameRef,
+    ) -> Result<JsImport, Error> {
         // Similar to `--target no-modules`, only allow vendor prefixes
         // basically for web apis, shouldn't be necessary for things like npm
         // packages or other imported items.
-        let vendor_prefixes = self.vendor_prefixes.get(item);
-        if let Some(vendor_prefixes) = vendor_prefixes {
-            assert!(!vendor_prefixes.is_empty());
-
-            if let Some(decode::ImportModule::Inline(_) | decode::ImportModule::Named(_)) =
-                &import.module
-            {
-                bail!(
-                    "local JS snippets do not support vendor prefixes for \
+        if let NameRef::Identifier(item) = item {
+            let vendor_prefixes = self.vendor_prefixes.get(item);
+            if let Some(vendor_prefixes) = vendor_prefixes {
+                assert!(!vendor_prefixes.is_empty());
+
+                if let Some(decode::ImportModule::Inline(_) | decode::ImportModule::Named(_)) =
+                    &import.module
+                {
+                    bail!(
+                        "local JS snippets do not support vendor prefixes for \
                      the import of `{}` with a polyfill of `{}`",
-                    item,
-                    &vendor_prefixes[0]
-                );
-            }
-            if let Some(decode::ImportModule::RawNamed(module)) = &import.module {
-                bail!(
-                    "import of `{}` from `{}` has a polyfill of `{}` listed, but
+                        item,
+                        &vendor_prefixes[0]
+                    );
+                }
+                if let Some(decode::ImportModule::RawNamed(module)) = &import.module {
+                    bail!(
+                        "import of `{}` from `{}` has a polyfill of `{}` listed, but
                      vendor prefixes aren't supported when importing from modules",
-                    item,
-                    module,
-                    &vendor_prefixes[0],
-                );
-            }
-            if let Some(ns) = &import.js_namespace {
-                bail!(
-                    "import of `{}` through js namespace `{}` isn't supported \
+                        item,
+                        module,
+                        &vendor_prefixes[0],
+                    );
+                }
+                if let Some(ns) = &import.js_namespace {
+                    bail!(
+                        "import of `{}` through js namespace `{}` isn't supported \
                      right now when it lists a polyfill",
-                    item,
-                    ns.join(".")
-                );
+                        item,
+                        ns.join(".")
+                    );
+                }
+                return Ok(JsImport {
+                    name: JsImportName::VendorPrefixed {
+                        name: item.to_string(),
+                        prefixes: vendor_prefixes.clone(),
+                    },
+                    fields: Vec::new(),
+                });
             }
-            return Ok(JsImport {
-                name: JsImportName::VendorPrefixed {
-                    name: item.to_string(),
-                    prefixes: vendor_prefixes.clone(),
-                },
-                fields: Vec::new(),
-            });
         }
 
         let (name, fields) = match import.js_namespace {
             Some(ref ns) => {
-                let mut tail = ns[1..].to_owned();
-                tail.push(item.to_string());
+                let mut tail: Vec<_> = ns[1..]
+                    .iter()
+                    .map(|s| AuxName::Identifier(s.to_string()))
+                    .collect();
+                tail.push(AuxName::from_ref(item));
                 (ns[0].to_owned(), tail)
             }
-            None => (item.to_owned(), Vec::new()),
+            None => (item.free_function().to_owned(), Vec::new()),
         };
 
         let name = match import.module {
diff --git a/crates/cli-support/src/wit/nonstandard.rs b/crates/cli-support/src/wit/nonstandard.rs
index ac0f6d6eb6b..55da05716f5 100644
--- a/crates/cli-support/src/wit/nonstandard.rs
+++ b/crates/cli-support/src/wit/nonstandard.rs
@@ -1,3 +1,4 @@
+use crate::decode;
 use crate::intrinsic::Intrinsic;
 use crate::wit::AdapterId;
 use std::borrow::Cow;
@@ -66,6 +67,12 @@ pub struct WasmBindgenAux {
 
 pub type WasmBindgenAuxId = TypedCustomSectionId<WasmBindgenAux>;
 
+#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub enum AuxName {
+    Identifier(String),
+    Symbol(String),
+}
+
 #[derive(Debug)]
 pub struct AuxExport {
     /// When generating errors about this export, a helpful name to remember it
@@ -126,7 +133,7 @@ pub enum AuxExportKind {
     /// Rust object in the JS heap.
     Method {
         class: String,
-        name: String,
+        name: AuxName,
         receiver: AuxReceiverKind,
         kind: AuxExportedMethodKind,
     },
@@ -221,7 +228,7 @@ pub enum AuxImport {
 
     /// A static method on a class is being imported, and the `this` of the
     /// function call is expected to always be the class.
-    ValueWithThis(JsImport, String),
+    ValueWithThis(JsImport, AuxName),
 
     /// This import is expected to be a function that takes an `externref` and
     /// returns a `bool`. It's expected that it tests if the argument is an
@@ -250,31 +257,31 @@ pub enum AuxImport {
     /// This import is expected to be a shim that simply calls the `foo` method
     /// on the first object, passing along all other parameters and returning
     /// the resulting value.
-    StructuralMethod(String),
+    StructuralMethod(AuxName),
 
     /// This import is a "structural getter" which simply returns the `.field`
     /// value of the first argument as an object.
     ///
     /// e.g. `function(x) { return x.foo; }`
-    StructuralGetter(String),
+    StructuralGetter(AuxName),
 
     /// This import is a "structural getter" which simply returns the `.field`
     /// value of the specified class
     ///
     /// e.g. `function() { return TheClass.foo; }`
-    StructuralClassGetter(JsImport, String),
+    StructuralClassGetter(JsImport, AuxName),
 
     /// This import is a "structural setter" which simply sets the `.field`
     /// value of the first argument to the second argument.
     ///
     /// e.g. `function(x, y) { x.foo = y; }`
-    StructuralSetter(String),
+    StructuralSetter(AuxName),
 
     /// This import is a "structural setter" which simply sets the `.field`
     /// value of the specified class to the first argument of the function.
     ///
     /// e.g. `function(x) { TheClass.foo = x; }`
-    StructuralClassSetter(JsImport, String),
+    StructuralClassSetter(JsImport, AuxName),
 
     /// This import is expected to be a shim that is an indexing getter of the
     /// JS class here, where the first argument of the function is the field to
@@ -369,17 +376,17 @@ pub enum AuxValue {
 
     /// A getter function for the class listed for the field, acquired using
     /// `getOwnPropertyDescriptor`.
-    Getter(JsImport, String),
+    Getter(JsImport, AuxName),
 
     /// Like `Getter`, but accesses a field of a class instead of an instance
     /// of the class.
-    ClassGetter(JsImport, String),
+    ClassGetter(JsImport, AuxName),
 
     /// Like `Getter`, except the `set` property.
-    Setter(JsImport, String),
+    Setter(JsImport, AuxName),
 
     /// Like `Setter`, but for class fields instead of instance fields.
-    ClassSetter(JsImport, String),
+    ClassSetter(JsImport, AuxName),
 }
 
 /// What can actually be imported and typically a value in each of the variants
@@ -394,7 +401,7 @@ pub struct JsImport {
     pub name: JsImportName,
     /// Various field accesses (like `.foo.bar.baz`) to hang off the `name`
     /// above.
-    pub fields: Vec<String>,
+    pub fields: Vec<AuxName>,
 }
 
 /// Return value of `determine_import` which is where we look at an imported
@@ -456,3 +463,19 @@ impl walrus::CustomSection for WasmBindgenAux {
         }
     }
 }
+
+impl AuxName {
+    pub fn as_ref(&self) -> wasm_bindgen_shared::NameRef<'_> {
+        match self {
+            AuxName::Identifier(s) => wasm_bindgen_shared::NameRef::Identifier(s),
+            AuxName::Symbol(s) => wasm_bindgen_shared::NameRef::Symbol(s),
+        }
+    }
+
+    pub fn from_ref(name: wasm_bindgen_shared::NameRef<'_>) -> Self {
+        match name {
+            wasm_bindgen_shared::NameRef::Identifier(s) => AuxName::Identifier(s.to_string()),
+            wasm_bindgen_shared::NameRef::Symbol(s) => AuxName::Symbol(s.to_string()),
+        }
+    }
+}
diff --git a/crates/cli/tests/reference/symbol.d.ts b/crates/cli/tests/reference/symbol.d.ts
new file mode 100644
index 00000000000..d0c922a0f71
--- /dev/null
+++ b/crates/cli/tests/reference/symbol.d.ts
@@ -0,0 +1,13 @@
+/* tslint:disable */
+/* eslint-disable */
+export class Foo {
+  free(): void;
+  /**
+   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive
+   */
+  [Symbol.toPrimitive](): string;
+/**
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
+ */
+  readonly [Symbol.toStringTag]: string;
+}
diff --git a/crates/cli/tests/reference/symbol.js b/crates/cli/tests/reference/symbol.js
new file mode 100644
index 00000000000..8db90187740
--- /dev/null
+++ b/crates/cli/tests/reference/symbol.js
@@ -0,0 +1,215 @@
+let wasm;
+export function __wbg_set_wasm(val) {
+    wasm = val;
+}
+
+
+const heap = new Array(128).fill(undefined);
+
+heap.push(undefined, null, true, false);
+
+function getObject(idx) { return heap[idx]; }
+
+const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder;
+
+let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
+
+cachedTextDecoder.decode();
+
+let cachedUint8ArrayMemory0 = null;
+
+function getUint8ArrayMemory0() {
+    if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
+        cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
+    }
+    return cachedUint8ArrayMemory0;
+}
+
+function getStringFromWasm0(ptr, len) {
+    ptr = ptr >>> 0;
+    return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
+}
+
+function notDefined(what) { return () => { throw new Error(`${what} is not defined`); }; }
+
+let heap_next = heap.length;
+
+function addHeapObject(obj) {
+    if (heap_next === heap.length) heap.push(heap.length + 1);
+    const idx = heap_next;
+    heap_next = heap[idx];
+
+    heap[idx] = obj;
+    return idx;
+}
+
+let WASM_VECTOR_LEN = 0;
+
+const lTextEncoder = typeof TextEncoder === 'undefined' ? (0, module.require)('util').TextEncoder : TextEncoder;
+
+let cachedTextEncoder = new lTextEncoder('utf-8');
+
+const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
+    ? function (arg, view) {
+    return cachedTextEncoder.encodeInto(arg, view);
+}
+    : function (arg, view) {
+    const buf = cachedTextEncoder.encode(arg);
+    view.set(buf);
+    return {
+        read: arg.length,
+        written: buf.length
+    };
+});
+
+function passStringToWasm0(arg, malloc, realloc) {
+
+    if (realloc === undefined) {
+        const buf = cachedTextEncoder.encode(arg);
+        const ptr = malloc(buf.length, 1) >>> 0;
+        getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
+        WASM_VECTOR_LEN = buf.length;
+        return ptr;
+    }
+
+    let len = arg.length;
+    let ptr = malloc(len, 1) >>> 0;
+
+    const mem = getUint8ArrayMemory0();
+
+    let offset = 0;
+
+    for (; offset < len; offset++) {
+        const code = arg.charCodeAt(offset);
+        if (code > 0x7F) break;
+        mem[ptr + offset] = code;
+    }
+
+    if (offset !== len) {
+        if (offset !== 0) {
+            arg = arg.slice(offset);
+        }
+        ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
+        const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
+        const ret = encodeString(arg, view);
+
+        offset += ret.written;
+        ptr = realloc(ptr, len, offset, 1) >>> 0;
+    }
+
+    WASM_VECTOR_LEN = offset;
+    return ptr;
+}
+
+let cachedDataViewMemory0 = null;
+
+function getDataViewMemory0() {
+    if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
+        cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
+    }
+    return cachedDataViewMemory0;
+}
+
+function dropObject(idx) {
+    if (idx < 132) return;
+    heap[idx] = heap_next;
+    heap_next = idx;
+}
+
+function takeObject(idx) {
+    const ret = getObject(idx);
+    dropObject(idx);
+    return ret;
+}
+
+const FooFinalization = (typeof FinalizationRegistry === 'undefined')
+    ? { register: () => {}, unregister: () => {} }
+    : new FinalizationRegistry(ptr => wasm.__wbg_foo_free(ptr >>> 0, 1));
+
+export class Foo {
+
+    __destroy_into_raw() {
+        const ptr = this.__wbg_ptr;
+        this.__wbg_ptr = 0;
+        FooFinalization.unregister(this);
+        return ptr;
+    }
+
+    free() {
+        const ptr = this.__destroy_into_raw();
+        wasm.__wbg_foo_free(ptr, 0);
+    }
+    /**
+     * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive
+     * @returns {string}
+     */
+    [Symbol.toPrimitive]() {
+        let deferred1_0;
+        let deferred1_1;
+        try {
+            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+            wasm.foo_Symbol_toPrimitive(retptr, this.__wbg_ptr);
+            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
+            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
+            deferred1_0 = r0;
+            deferred1_1 = r1;
+            return getStringFromWasm0(r0, r1);
+        } finally {
+            wasm.__wbindgen_add_to_stack_pointer(16);
+            wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
+        }
+    }
+    /**
+     * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
+     * @returns {string}
+     */
+    get [Symbol.toStringTag]() {
+        let deferred1_0;
+        let deferred1_1;
+        try {
+            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
+            wasm.foo_to_string_tag(retptr, this.__wbg_ptr);
+            var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
+            var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
+            deferred1_0 = r0;
+            deferred1_1 = r1;
+            return getStringFromWasm0(r0, r1);
+        } finally {
+            wasm.__wbindgen_add_to_stack_pointer(16);
+            wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
+        }
+    }
+}
+
+export function __wbg_Symboliterator_829a0523d5289487(arg0) {
+    const ret = getObject(arg0)[Symbol.iterator]();
+    return ret;
+};
+
+export function __wbg_SymboltoPrimitive_02f2dde8e44e86f2(arg0, arg1) {
+    SomeClass[Symbol.toPrimitive](getStringFromWasm0(arg0, arg1));
+};
+
+export const __wbg_importstaticsymbolgetter_39e661ab5756c496 = typeof SomeClass.import_static_symbol_getter == 'function' ? SomeClass.import_static_symbol_getter : notDefined('SomeClass.import_static_symbol_getter');
+
+export function __wbg_new_384bf245d5809064() {
+    const ret = new JsString();
+    return addHeapObject(ret);
+};
+
+export function __wbg_stringgetter_21c66c9c26971b36(arg0, arg1) {
+    const ret = getObject(arg1)[Symbol.toPrimitive];
+    const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
+    const len1 = WASM_VECTOR_LEN;
+    getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
+    getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
+};
+
+export function __wbindgen_object_drop_ref(arg0) {
+    takeObject(arg0);
+};
+
+export function __wbindgen_throw(arg0, arg1) {
+    throw new Error(getStringFromWasm0(arg0, arg1));
+};
+
diff --git a/crates/cli/tests/reference/symbol.rs b/crates/cli/tests/reference/symbol.rs
new file mode 100644
index 00000000000..357966d8975
--- /dev/null
+++ b/crates/cli/tests/reference/symbol.rs
@@ -0,0 +1,57 @@
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+extern "C" {
+    #[wasm_bindgen(js_namespace = SomeClass, js_name = Symbol.toPrimitive)]
+    fn import_static_symbol(s: &str);
+
+    #[wasm_bindgen(js_namespace = SomeClass, getter = Symbol.iterator)]
+    fn import_static_symbol_getter();
+
+    // We don't want to import JS strings as `String`, since Rust already has a
+    // `String` type in its prelude, so rename it as `JsString`.
+    #[wasm_bindgen(js_name = String)]
+    type JsString;
+
+    // This is a method on the JavaScript "String" class, so specify that with
+    // the `js_class` attribute.
+    #[wasm_bindgen(constructor)]
+    fn new() -> JsString;
+
+    // This is a method on the JavaScript "String" class, so specify that with
+    // the `js_class` attribute.
+    #[wasm_bindgen(method, js_class = "String", js_name = Symbol.iterator)]
+    fn string_iterator(this: &JsString) -> u32;
+
+    // This is a method on the JavaScript "String" class, so specify that with
+    // the `js_class` attribute.
+    #[wasm_bindgen(method, js_class = "String", getter = Symbol.toPrimitive)]
+    fn string_getter(this: &JsString) -> String;
+}
+
+fn make_used() {
+    import_static_symbol("");
+    import_static_symbol_getter();
+    let s = JsString::new();
+    s.string_iterator();
+    s.string_getter();
+}
+
+#[wasm_bindgen]
+pub struct Foo;
+
+#[wasm_bindgen]
+impl Foo {
+    /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive
+    #[wasm_bindgen(js_name = Symbol.toPrimitive)]
+    pub fn to_primitive(&self) -> String {
+        make_used();
+        "Why is it this string? I don't known.".to_string()
+    }
+
+    /// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
+    #[wasm_bindgen(getter = Symbol.toStringTag)]
+    pub fn to_string_tag(&self) -> String {
+        "RustFooClass".to_string()
+    }
+}
diff --git a/crates/cli/tests/reference/symbol.wat b/crates/cli/tests/reference/symbol.wat
new file mode 100644
index 00000000000..dec7b9335ca
--- /dev/null
+++ b/crates/cli/tests/reference/symbol.wat
@@ -0,0 +1,25 @@
+(module $reference_test.wasm
+  (type (;0;) (func (param i32) (result i32)))
+  (type (;1;) (func (param i32 i32)))
+  (type (;2;) (func (param i32 i32) (result i32)))
+  (type (;3;) (func (param i32 i32 i32)))
+  (type (;4;) (func (param i32 i32 i32 i32) (result i32)))
+  (func $__wbindgen_realloc (;0;) (type 4) (param i32 i32 i32 i32) (result i32))
+  (func $__wbindgen_malloc (;1;) (type 2) (param i32 i32) (result i32))
+  (func $foo_Symbol_toPrimitive (;2;) (type 1) (param i32 i32))
+  (func $foo_to_string_tag (;3;) (type 1) (param i32 i32))
+  (func $__wbindgen_free (;4;) (type 3) (param i32 i32 i32))
+  (func $__wbg_foo_free (;5;) (type 1) (param i32 i32))
+  (func $__wbindgen_add_to_stack_pointer (;6;) (type 0) (param i32) (result i32))
+  (memory (;0;) 17)
+  (export "memory" (memory 0))
+  (export "__wbg_foo_free" (func $__wbg_foo_free))
+  (export "foo_Symbol_toPrimitive" (func $foo_Symbol_toPrimitive))
+  (export "foo_to_string_tag" (func $foo_to_string_tag))
+  (export "__wbindgen_malloc" (func $__wbindgen_malloc))
+  (export "__wbindgen_realloc" (func $__wbindgen_realloc))
+  (export "__wbindgen_add_to_stack_pointer" (func $__wbindgen_add_to_stack_pointer))
+  (export "__wbindgen_free" (func $__wbindgen_free))
+  (@custom "target_features" (after code) "\02+\0fmutable-globals+\08sign-ext")
+)
+
diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs
index 39638bca4fc..7de3835eca0 100644
--- a/crates/macro-support/src/parser.rs
+++ b/crates/macro-support/src/parser.rs
@@ -49,6 +49,28 @@ struct AttributeParseState {
     unused_attrs: RefCell<Vec<Ident>>,
 }
 
+#[derive(Debug, Clone)]
+enum JsName {
+    Identifier(String),
+    Symbol(String),
+}
+
+impl JsName {
+    fn to_ast(&self) -> ast::Name {
+        match self {
+            JsName::Identifier(s) => ast::Name::Identifier(s.clone()),
+            JsName::Symbol(s) => ast::Name::Symbol(s.clone()),
+        }
+    }
+
+    fn as_ref(&self) -> shared::NameRef<'_> {
+        match self {
+            JsName::Identifier(s) => shared::NameRef::Identifier(s.as_str()),
+            JsName::Symbol(s) => shared::NameRef::Symbol(s.as_str()),
+        }
+    }
+}
+
 /// Parsed attributes from a `#[wasm_bindgen(..)]`.
 #[cfg_attr(feature = "extra-traits", derive(Debug))]
 pub struct BindgenAttrs {
@@ -67,15 +89,15 @@ macro_rules! attrgen {
             (module, Module(Span, String, Span)),
             (raw_module, RawModule(Span, String, Span)),
             (inline_js, InlineJs(Span, String, Span)),
-            (getter, Getter(Span, Option<String>)),
-            (setter, Setter(Span, Option<String>)),
+            (getter, Getter(Span, Option<JsName>)),
+            (setter, Setter(Span, Option<JsName>)),
             (indexing_getter, IndexingGetter(Span)),
             (indexing_setter, IndexingSetter(Span)),
             (indexing_deleter, IndexingDeleter(Span)),
             (structural, Structural(Span)),
             (r#final, Final(Span)),
             (readonly, Readonly(Span)),
-            (js_name, JsName(Span, String, Span)),
+            (js_name, JsName(Span, JsName, Span)),
             (js_class, JsClass(Span, String, Span)),
             (inspectable, Inspectable(Span)),
             (is_type_of, IsTypeOf(Span, syn::Expr)),
@@ -159,6 +181,20 @@ macro_rules! methods {
         }
     };
 
+    (@method $name:ident, $variant:ident(Span, JsName, Span)) => {
+        fn $name(&self) -> Option<(&JsName, Span)> {
+            self.attrs
+                .iter()
+                .find_map(|a| match &a.1 {
+                    BindgenAttr::$variant(_, s, span) => {
+                        a.0.set(true);
+                        Some((s, *span))
+                    }
+                    _ => None,
+                })
+        }
+    };
+
     (@method $name:ident, $variant:ident(Span, Vec<String>, Vec<Span>)) => {
         fn $name(&self) -> Option<(&[String], &[Span])> {
             self.attrs
@@ -236,6 +272,35 @@ impl BindgenAttrs {
         }
     }
 
+    /// Returns the specified `js_name` if it is a simple identifier, or an error if
+    /// it is a symbol. If `js_name` is not specified, returns the default name.
+    fn get_js_name_identifier(
+        &self,
+        default_identifier: impl FnOnce() -> String,
+        symbols_not_supported: &str,
+    ) -> Result<String, Diagnostic> {
+        if let Some((name, span)) = self.js_name() {
+            match name {
+                JsName::Identifier(name) => Ok(name.clone()),
+                JsName::Symbol(_) => {
+                    eprintln!("{}", symbols_not_supported);
+                    Err(Diagnostic::span_error(span, symbols_not_supported))
+                }
+            }
+        } else {
+            Ok(default_identifier())
+        }
+    }
+
+    /// Returns the specified `js_name` if it is a simple identifier, or an error if
+    /// it is a symbol. If `js_name` is not specified, returns the default name.
+    fn get_js_name(&self, default_identifier: impl FnOnce() -> String) -> JsName {
+        self.js_name().map_or_else(
+            || JsName::Identifier(default_identifier()),
+            |(name, _)| name.clone(),
+        )
+    }
+
     attrgen!(methods);
 }
 
@@ -340,6 +405,43 @@ impl Parse for BindgenAttr {
                 return Ok(BindgenAttr::$variant(attr_span, val, span))
             });
 
+            (@parser $variant:ident(Span, JsName, Span)) => ({
+                input.parse::<Token![=]>()?;
+                let (val, span) = match input.parse::<syn::LitStr>() {
+                    Ok(str) => (JsName::Identifier(str.value()), str.span()),
+                    Err(_) => {
+                        let ident = input.parse::<AnyIdent>()?.0;
+                        if ident == "Symbol" && input.parse::<Token![.]>().is_ok(){
+                            let ident = input.parse::<AnyIdent>()?.0;
+                            (JsName::Symbol(ident.to_string()), ident.span())
+                        } else {
+                            (JsName::Identifier(ident.to_string()), ident.span())
+                        }
+                    }
+                };
+                return Ok(BindgenAttr::$variant(attr_span, val, span))
+            });
+
+            (@parser $variant:ident(Span, Option<JsName>)) => ({
+                if input.parse::<Token![=]>().is_ok() {
+                    let val = match input.parse::<syn::LitStr>() {
+                        Ok(str) => JsName::Identifier(str.value()),
+                        Err(_) => {
+                            let ident = input.parse::<AnyIdent>()?.0;
+                            if ident == "Symbol" && input.parse::<Token![.]>().is_ok(){
+                                let ident = input.parse::<AnyIdent>()?.0;
+                                JsName::Symbol(ident.to_string())
+                            } else {
+                                JsName::Identifier(ident.to_string())
+                            }
+                        }
+                    };
+                    return Ok(BindgenAttr::$variant(attr_span, Some(val)))
+                } else {
+                    return Ok(BindgenAttr::$variant(attr_span, None));
+                }
+            });
+
             (@parser $variant:ident(Span, Vec<String>, Vec<Span>)) => ({
                 input.parse::<Token![=]>()?;
                 let (vals, spans) = match input.parse::<syn::ExprArray>() {
@@ -419,10 +521,10 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs)> for &'a mut syn::ItemStruct
             );
         }
         let mut fields = Vec::new();
-        let js_name = attrs
-            .js_name()
-            .map(|s| s.0.to_string())
-            .unwrap_or(self.ident.unraw().to_string());
+        let js_name = attrs.get_js_name_identifier(
+            || self.ident.unraw().to_string(),
+            "structs with #[wasm_bindgen] do not support symbols in js_name",
+        )?;
         let is_inspectable = attrs.inspectable().is_some();
         let getter_with_clone = attrs.getter_with_clone();
         for (i, field) in self.fields.iter_mut().enumerate() {
@@ -441,18 +543,14 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs)> for &'a mut syn::ItemStruct
                 continue;
             }
 
-            let js_field_name = match attrs.js_name() {
-                Some((name, _)) => name.to_string(),
-                None => js_field_name,
-            };
-
+            let js_field_name = attrs.get_js_name(|| js_field_name);
             let comments = extract_doc_comments(&field.attrs);
-            let getter = shared::struct_field_get(&js_name, &js_field_name);
-            let setter = shared::struct_field_set(&js_name, &js_field_name);
+            let getter = shared::struct_field_get(&js_name, js_field_name.as_ref());
+            let setter = shared::struct_field_set(&js_name, js_field_name.as_ref());
 
             fields.push(ast::StructField {
                 rust_name: member,
-                js_name: js_field_name,
+                js_name: js_field_name.to_ast(),
                 struct_name: self.ident.clone(),
                 readonly: attrs.readonly().is_some(),
                 ty: field.ty.clone(),
@@ -622,6 +720,8 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule
             format!(
                 "__wbg_{}_{}",
                 wasm.name
+                    .as_ref()
+                    .disambiguated_name()
                     .chars()
                     .filter(|c| c.is_ascii_alphanumeric())
                     .collect::<String>(),
@@ -702,10 +802,10 @@ impl ConvertToAst<(&ast::Program, BindgenAttrs)> for syn::ForeignItemType {
         self,
         (program, attrs): (&ast::Program, BindgenAttrs),
     ) -> Result<Self::Target, Diagnostic> {
-        let js_name = attrs
-            .js_name()
-            .map(|s| s.0)
-            .map_or_else(|| self.ident.to_string(), |s| s.to_string());
+        let js_name = attrs.get_js_name_identifier(
+            || self.ident.to_string(),
+            "extern types with #[wasm_bindgen] do not support symbols in js_name",
+        )?;
         let typescript_type = attrs.typescript_type().map(|s| s.0.to_string());
         let is_type_of = attrs.is_type_of().cloned();
         let shim = format!("__wbg_instanceof_{}_{}", self.ident, ShortHash(&self.ident));
@@ -762,13 +862,10 @@ impl<'a> ConvertToAst<(&ast::Program, BindgenAttrs, &'a Option<ast::ImportModule
                 "static strings require a string literal",
             ));
         }
-
-        let default_name = self.ident.to_string();
-        let js_name = opts
-            .js_name()
-            .map(|p| p.0)
-            .unwrap_or(&default_name)
-            .to_string();
+        let js_name = opts.get_js_name_identifier(
+            || self.ident.to_string(),
+            "statics with #[wasm_bindgen] do not support symbols in js_name",
+        )?;
         let shim = format!(
             "__wbg_static_accessor_{}_{}",
             self.ident,
@@ -1000,39 +1097,36 @@ fn function_from_decl(
         syn::ReturnType::Type(_, ty) => Some(replace_self(*ty)),
     };
 
-    let (name, name_span, renamed_via_js_name) =
-        if let Some((js_name, js_name_span)) = opts.js_name() {
-            let kind = operation_kind(opts);
-            let prefix = match kind {
-                OperationKind::Setter(_) => "set_",
-                _ => "",
-            };
-            let name = if prefix.is_empty()
-                && opts.method().is_none()
-                && is_js_keyword(js_name, skip_keywords)
-            {
-                format!("_{}", js_name)
-            } else {
-                format!("{}{}", prefix, js_name)
-            };
-            (name, js_name_span, true)
-        } else {
-            let name = if !is_from_impl
-                && opts.method().is_none()
-                && is_js_keyword(&decl_name.to_string(), skip_keywords)
-            {
-                format!("_{}", decl_name.unraw())
-            } else {
-                decl_name.unraw().to_string()
-            };
-            (name, decl_name.span(), false)
+    let (mut name, name_span) = if let Some((js_name, js_name_span)) = opts.js_name() {
+        let kind = operation_kind(opts);
+        let prefix = match kind {
+            OperationKind::Setter(_) => "set_",
+            _ => "",
         };
+        let mut name = js_name.clone();
+        if let JsName::Identifier(ref mut name) = name {
+            *name = format!("{}{}", prefix, name);
+        }
+        (name, js_name_span)
+    } else {
+        (
+            JsName::Identifier(decl_name.unraw().to_string()),
+            decl_name.span(),
+        )
+    };
+
+    // add underscore if the name is a keyword
+    if let JsName::Identifier(ref mut name) = name {
+        if !is_from_impl && opts.method().is_none() && is_js_keyword(&name, skip_keywords) {
+            *name = format!("_{}", name);
+        }
+    }
+
     Ok((
         ast::Function {
             arguments,
             name_span,
-            name,
-            renamed_via_js_name,
+            name: name.to_ast(),
             ret,
             rust_attrs: attrs,
             rust_vis: vis,
@@ -1403,10 +1497,10 @@ impl<'a> MacroParse<(&'a mut TokenStream, BindgenAttrs)> for syn::ItemEnum {
         }
 
         let generate_typescript = opts.skip_typescript().is_none();
-        let js_name = opts
-            .js_name()
-            .map(|s| s.0)
-            .map_or_else(|| self.ident.to_string(), |s| s.to_string());
+        let js_name = opts.get_js_name_identifier(
+            || self.ident.to_string(),
+            "enums with #[wasm_bindgen] do not support symbols in js_name",
+        )?;
         let comments = extract_doc_comments(&self.attrs);
 
         opts.check_used();
@@ -1853,10 +1947,10 @@ pub fn check_unused_attrs(tokens: &mut TokenStream) {
 fn operation_kind(opts: &BindgenAttrs) -> ast::OperationKind {
     let mut operation_kind = ast::OperationKind::Regular;
     if let Some(g) = opts.getter() {
-        operation_kind = ast::OperationKind::Getter(g.clone());
+        operation_kind = ast::OperationKind::Getter(g.as_ref().map(|g| g.to_ast()));
     }
     if let Some(s) = opts.setter() {
-        operation_kind = ast::OperationKind::Setter(s.clone());
+        operation_kind = ast::OperationKind::Setter(s.as_ref().map(|s| s.to_ast()));
     }
     if opts.indexing_getter().is_some() {
         operation_kind = ast::OperationKind::IndexingGetter;
diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs
index 57faecfcd84..aa2419ff58d 100644
--- a/crates/shared/src/lib.rs
+++ b/crates/shared/src/lib.rs
@@ -54,6 +54,11 @@ macro_rules! shared_api {
             Enum(StringEnum<'a>),
         }
 
+        enum Name<'a> {
+            Identifier(&'a str),
+            Symbol(&'a str),
+        }
+
         struct ImportFunction<'a> {
             shim: &'a str,
             catch: bool,
@@ -81,8 +86,8 @@ macro_rules! shared_api {
 
         enum OperationKind<'a> {
             Regular,
-            Getter(&'a str),
-            Setter(&'a str),
+            Getter(Name<'a>),
+            Setter(Name<'a>),
             IndexingGetter,
             IndexingSetter,
             IndexingDeleter,
@@ -136,7 +141,7 @@ macro_rules! shared_api {
         struct Function<'a> {
             arg_names: Vec<String>,
             asyncness: bool,
-            name: &'a str,
+            name: Name<'a>,
             generate_typescript: bool,
             generate_jsdoc: bool,
             variadic: bool,
@@ -151,7 +156,7 @@ macro_rules! shared_api {
         }
 
         struct StructField<'a> {
-            name: &'a str,
+            name: Name<'a>,
             readonly: bool,
             comments: Vec<&'a str>,
             generate_typescript: bool,
@@ -188,33 +193,70 @@ pub fn unwrap_function(struct_name: &str) -> String {
     name
 }
 
-pub fn free_function_export_name(function_name: &str) -> String {
-    function_name.to_string()
+#[derive(Debug, Hash)]
+pub enum NameRef<'a> {
+    Identifier(&'a str),
+    Symbol(&'a str),
+}
+
+impl NameRef<'_> {
+    /// Returns the identifier name of a free function.
+    ///
+    /// If the name is a symbol, this will panic.
+    pub fn free_function(&self) -> &str {
+        match self {
+            NameRef::Identifier(name) => name,
+            _ => {
+                panic!(
+                    "The name of a free function name cannot be a symbol: {}",
+                    self.debug_name()
+                )
+            }
+        }
+    }
+
+    pub fn debug_name(&self) -> String {
+        match self {
+            NameRef::Identifier(name) => name.to_string(),
+            NameRef::Symbol(name) => format!("Symbol.{name}"),
+        }
+    }
+
+    pub fn disambiguated_name(&self) -> String {
+        match self {
+            NameRef::Identifier(s) => s.to_string(),
+            NameRef::Symbol(s) => format!("Symbol_{s}"),
+        }
+    }
+}
+
+pub fn free_function_export_name(function_name: NameRef) -> String {
+    function_name.free_function().to_string()
 }
 
-pub fn struct_function_export_name(struct_: &str, f: &str) -> String {
+pub fn struct_function_export_name(struct_: &str, f: NameRef) -> String {
     let mut name = struct_
         .chars()
         .flat_map(|s| s.to_lowercase())
         .collect::<String>();
     name.push('_');
-    name.push_str(f);
+    name.push_str(&f.disambiguated_name());
     name
 }
 
-pub fn struct_field_get(struct_: &str, f: &str) -> String {
+pub fn struct_field_get(struct_: &str, f: NameRef) -> String {
     let mut name = String::from("__wbg_get_");
     name.extend(struct_.chars().flat_map(|s| s.to_lowercase()));
     name.push('_');
-    name.push_str(f);
+    name.push_str(&f.disambiguated_name());
     name
 }
 
-pub fn struct_field_set(struct_: &str, f: &str) -> String {
+pub fn struct_field_set(struct_: &str, f: NameRef) -> String {
     let mut name = String::from("__wbg_set_");
     name.extend(struct_.chars().flat_map(|s| s.to_lowercase()));
     name.push('_');
-    name.push_str(f);
+    name.push_str(&f.disambiguated_name());
     name
 }