Repository: dprint/dprint-plugin-typescript (rust)

Issue:
TypeScript: Comma as separator for single-line type literal
**Is your feature request related to a problem? Please describe.**

I want to write this code:

```typescript
type MyObject = { foo: number, bar: string }
```

But dprint-plugin-typescript converts it to semicolons:

```typescript
type MayObject = { foo: number; bar: string }
```

which it is not what I want.

**Describe the solution you'd like**

While I prefer commas as separator, others might prefer semicolons, so I suggest a configuration key should be introduced to specify whether the separator should be commas or semicolons:
* `objectLiteralTypeSeparator: 'comma' | 'semicolon'`
* `singleLineObjectLiteralTypeSeparator: 'comma' | 'semicolon''`
* `multiLineObjectLiteralTypeSeparator: 'comma' | 'semicolon' | 'none'`

**Describe alternatives you've considered**

**Additional context**

I have set `.semi_colons(SemiColons::Asi)` in [my own formatter](https://github.com/KSXGitHub/sane-fmt/blob/0.2.16/src/rules/mod.rs#L14), so I would prefer multi-line object literal type to **not** have separator.

Pull request description:
Support using a comma as separator in type literals
Adds the following configuration:

* `typeLiteral.separatorKind`
* `typeLiteral.separatorKind.singleLine`
* `typeLiteral.separatorKind.multiLine`

With possible values "semiColons" or "commas". So for example:

```ts
"semiColons": "asi",
"typeLiteral.separatorKind.singleLine": "commas",
```

TODO:

- [ ] Update schema file with changes.

Closes #11

Patch:
diff --git a/src/configuration/builder.rs b/src/configuration/builder.rs
index 2ab5f3c..dba994e 100644
--- a/src/configuration/builder.rs
+++ b/src/configuration/builder.rs
@@ -386,6 +386,21 @@ impl ConfigurationBuilder {
         self.insert("memberExpression.linePerExpression", value.into())
     }
 
+    /// The kind of separator to use in type literals.
+    pub fn type_literal_separator_kind(&mut self, value: SemiColonsOrCommas) -> &mut Self {
+        self.insert("typeLiteral.separatorKind", value.to_string().into())
+    }
+
+    /// The kind of separator to use in type literals when single line.
+    pub fn type_literal_separator_kind_single_line(&mut self, value: SemiColonsOrCommas) -> &mut Self {
+        self.insert("typeLiteral.separatorKind.singleLine", value.to_string().into())
+    }
+
+    /// The kind of separator to use in type literals when multi-line.
+    pub fn type_literal_separator_kind_multi_line(&mut self, value: SemiColonsOrCommas) -> &mut Self {
+        self.insert("typeLiteral.separatorKind.multiLine", value.to_string().into())
+    }
+
     /* ignore comments */
 
     /// The text to use for an ignore comment (ex. `// dprint-ignore`).
@@ -676,6 +691,11 @@ impl ConfigurationBuilder {
         self.insert("tupleType.trailingCommas", value.to_string().into())
     }
 
+    /// Only applies when using commas on type literals.
+    pub fn type_literal_trailing_commas(&mut self, value: TrailingCommas) -> &mut Self {
+        self.insert("typeLiteral.trailingCommas", value.to_string().into())
+    }
+
     pub fn type_parameters_trailing_commas(&mut self, value: TrailingCommas) -> &mut Self {
         self.insert("typeParameters.trailingCommas", value.to_string().into())
     }
@@ -836,6 +856,9 @@ mod tests {
             .arrow_function_use_parentheses(UseParentheses::Maintain)
             .binary_expression_line_per_expression(false)
             .member_expression_line_per_expression(false)
+            .type_literal_separator_kind(SemiColonsOrCommas::Commas)
+            .type_literal_separator_kind_single_line(SemiColonsOrCommas::Commas)
+            .type_literal_separator_kind_multi_line(SemiColonsOrCommas::Commas)
             /* ignore comments */
             .ignore_node_comment_text("ignore")
             .ignore_file_comment_text("ignore-file")
@@ -912,6 +935,7 @@ mod tests {
             .object_pattern_trailing_commas(TrailingCommas::Never)
             .type_parameters_trailing_commas(TrailingCommas::Never)
             .tuple_type_trailing_commas(TrailingCommas::Never)
+            .type_literal_trailing_commas(TrailingCommas::Never)
             /* use braces */
             .if_statement_use_braces(UseBraces::Always)
             .for_statement_use_braces(UseBraces::Always)
@@ -970,7 +994,7 @@ mod tests {
             .while_statement_space_after_while_keyword(true);
 
         let inner_config = config.get_inner_config();
-        assert_eq!(inner_config.len(), 137);
+        assert_eq!(inner_config.len(), 141);
         let diagnostics = resolve_config(inner_config, &resolve_global_config(HashMap::new()).config).diagnostics;
         assert_eq!(diagnostics.len(), 0);
     }
diff --git a/src/configuration/resolve_config.rs b/src/configuration/resolve_config.rs
index c78874d..59e68c8 100644
--- a/src/configuration/resolve_config.rs
+++ b/src/configuration/resolve_config.rs
@@ -41,6 +41,7 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
     let use_braces = get_value(&mut config, "useBraces", UseBraces::WhenNotSingleLine, &mut diagnostics);
     let prefer_hanging = get_value(&mut config, "preferHanging", false, &mut diagnostics);
     let prefer_single_line = get_value(&mut config, "preferSingleLine", false, &mut diagnostics);
+    let type_literal_separator_kind = get_value(&mut config, "typeLiteral.separatorKind", SemiColonsOrCommas::SemiColons, &mut diagnostics);
 
     let resolved_config = Configuration {
         line_width: get_value(&mut config, "lineWidth", global_config.line_width.unwrap_or(DEFAULT_GLOBAL_CONFIGURATION.line_width), &mut diagnostics),
@@ -53,6 +54,8 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
         arrow_function_use_parentheses: get_value(&mut config, "arrowFunction.useParentheses", UseParentheses::Maintain, &mut diagnostics),
         binary_expression_line_per_expression: get_value(&mut config, "binaryExpression.linePerExpression", false, &mut diagnostics),
         member_expression_line_per_expression: get_value(&mut config, "memberExpression.linePerExpression", false, &mut diagnostics),
+        type_literal_separator_kind_single_line: get_value(&mut config, "typeLiteral.separatorKind.singleLine", type_literal_separator_kind, &mut diagnostics),
+        type_literal_separator_kind_multi_line: get_value(&mut config, "typeLiteral.separatorKind.multiLine", type_literal_separator_kind, &mut diagnostics),
         /* ignore comments */
         ignore_node_comment_text: get_value(&mut config, "ignoreNodeCommentText", String::from("dprint-ignore"), &mut diagnostics),
         ignore_file_comment_text: get_value(&mut config, "ignoreFileCommentText", String::from("dprint-ignore-file"), &mut diagnostics),
@@ -128,6 +131,7 @@ pub fn resolve_config(config: ConfigKeyMap, global_config: &GlobalConfiguration)
         object_expression_trailing_commas: get_value(&mut config, "objectExpression.trailingCommas", trailing_commas, &mut diagnostics),
         object_pattern_trailing_commas: get_value(&mut config, "objectPattern.trailingCommas", trailing_commas, &mut diagnostics),
         tuple_type_trailing_commas: get_value(&mut config, "tupleType.trailingCommas", trailing_commas, &mut diagnostics),
+        type_literal_trailing_commas: get_value(&mut config, "typeLiteral.trailingCommas", trailing_commas, &mut diagnostics),
         type_parameters_trailing_commas: get_value(&mut config, "typeParameters.trailingCommas", trailing_commas, &mut diagnostics),
         /* use braces */
         if_statement_use_braces: get_value(&mut config, "ifStatement.useBraces", use_braces, &mut diagnostics),
diff --git a/src/configuration/types.rs b/src/configuration/types.rs
index fcb93cd..560b4e3 100644
--- a/src/configuration/types.rs
+++ b/src/configuration/types.rs
@@ -217,6 +217,22 @@ generate_str_to_from![
     [PreferSingle, "preferSingle"]
 ];
 
+/// Whether to use semi-colons or commas.
+#[derive(Clone, PartialEq, Copy, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum SemiColonsOrCommas {
+    /// Use semi colons (default).
+    SemiColons,
+    /// Use commas.
+    Commas,
+}
+
+generate_str_to_from![
+    SemiColonsOrCommas,
+    [SemiColons, "semiColons"],
+    [Commas, "commas"]
+];
+
 #[derive(Clone, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct Configuration {
@@ -233,6 +249,10 @@ pub struct Configuration {
     pub binary_expression_line_per_expression: bool,
     #[serde(rename = "memberExpression.linePerExpression")]
     pub member_expression_line_per_expression: bool,
+    #[serde(rename = "typeLiteral.separatorKind.singleLine")]
+    pub type_literal_separator_kind_single_line: SemiColonsOrCommas,
+    #[serde(rename = "typeLiteral.separatorKind.multiLine")]
+    pub type_literal_separator_kind_multi_line: SemiColonsOrCommas,
     /* ignore comments */
     pub ignore_node_comment_text: String,
     pub ignore_file_comment_text: String,
@@ -373,6 +393,8 @@ pub struct Configuration {
     pub object_expression_trailing_commas: TrailingCommas,
     #[serde(rename = "tupleType.trailingCommas")]
     pub tuple_type_trailing_commas: TrailingCommas,
+    #[serde(rename = "typeLiteral.trailingCommas")]
+    pub type_literal_trailing_commas: TrailingCommas,
     #[serde(rename = "typeParameters.trailingCommas")]
     pub type_parameters_trailing_commas: TrailingCommas,
     /* use braces */
diff --git a/src/parsing/parser.rs b/src/parsing/parser.rs
index 5422c3d..27b6089 100644
--- a/src/parsing/parser.rs
+++ b/src/parsing/parser.rs
@@ -690,8 +690,7 @@ fn parse_class_decl_or_expr<'a>(node: ClassDeclOrExpr<'a>, context: &mut Context
         should_use_blank_line: move |previous, next, context| {
             node_helpers::has_separating_blank_line(previous, next, context)
         },
-        trailing_commas: None,
-        semi_colons: None,
+        separator: Separator::none(),
     }, context));
 
     if node.is_class_expr {
@@ -764,8 +763,7 @@ fn parse_enum_decl<'a>(node: &'a TsEnumDecl, context: &mut Context<'a>) -> Print
                 MemberSpacing::Maintain => node_helpers::has_separating_blank_line(previous, next, context),
             }
         },
-        trailing_commas: Some(context.config.enum_declaration_trailing_commas),
-        semi_colons: None,
+        separator: context.config.enum_declaration_trailing_commas.into(),
     }, context));
 
     return items;
@@ -1090,8 +1088,7 @@ fn parse_module_or_namespace_decl<'a>(node: ModuleOrNamespaceDecl<'a>, context:
                         should_use_blank_line: move |previous, next, context| {
                             node_helpers::has_separating_blank_line(previous, next, context)
                         },
-                        trailing_commas: None,
-                        semi_colons: None,
+                        separator: Separator::none(),
                     }, context));
                 },
                 TsNamespaceBody::TsNamespaceDecl(decl) => {
@@ -1131,8 +1128,7 @@ fn parse_named_import_or_export_specifiers<'a>(parent: &Node<'a>, specifiers: Ve
     return parse_object_like_node(ParseObjectLikeNodeOptions {
         node_span_data: parent.span_data(),
         members: specifiers,
-        trailing_commas: Some(get_trailing_commas(parent, context)),
-        semi_colons: None,
+        separator: get_trailing_commas(parent, context).into(),
         prefer_hanging: get_prefer_hanging(parent, context),
         prefer_single_line: get_prefer_single_line(parent, context),
         surround_single_line_with_spaces: get_use_space(parent, context),
@@ -1937,8 +1933,7 @@ fn parse_object_lit<'a>(node: &'a ObjectLit, context: &mut Context<'a>) -> Print
     parse_object_like_node(ParseObjectLikeNodeOptions {
         node_span_data: node.span,
         members: node.props.iter().map(|x| x.into()).collect(),
-        trailing_commas: Some(context.config.object_expression_trailing_commas),
-        semi_colons: None,
+        separator: context.config.object_expression_trailing_commas.into(),
         prefer_hanging: context.config.object_expression_prefer_hanging,
         prefer_single_line: context.config.object_expression_prefer_single_line,
         surround_single_line_with_spaces: true,
@@ -1978,8 +1973,7 @@ fn parse_sequence_expr<'a>(node: &'a SeqExpr, context: &mut Context<'a>) -> Prin
         prefer_hanging: context.config.sequence_expression_prefer_hanging,
         force_use_new_lines: false,
         allow_blank_lines: false,
-        trailing_commas: Some(TrailingCommas::Never),
-        semi_colons: None,
+        separator: TrailingCommas::Never.into(),
         single_line_space_at_start: false,
         single_line_space_at_end: false,
         custom_single_line_separator: None,
@@ -2389,8 +2383,7 @@ fn parse_interface_body<'a>(node: &'a TsInterfaceBody, context: &mut Context<'a>
         should_use_blank_line: move |previous, next, context| {
             node_helpers::has_separating_blank_line(previous, next, context)
         },
-        trailing_commas: None,
-        semi_colons: Some(context.config.semi_colons),
+        separator: context.config.semi_colons.into(),
     }, context);
 
     fn get_parent_info(context: &mut Context) -> Option<Info> {
@@ -2404,15 +2397,24 @@ fn parse_interface_body<'a>(node: &'a TsInterfaceBody, context: &mut Context<'a>
 }
 
 fn parse_type_lit<'a>(node: &'a TsTypeLit, context: &mut Context<'a>) -> PrintItems {
-    parse_object_like_node(ParseObjectLikeNodeOptions {
+    return parse_object_like_node(ParseObjectLikeNodeOptions {
         node_span_data: node.span,
         members: node.members.iter().map(|m| m.into()).collect(),
-        trailing_commas: None,
-        semi_colons: Some(context.config.semi_colons),
+        separator: Separator {
+            single_line: Some(semi_colon_or_comma_to_separator_value(context.config.type_literal_separator_kind_single_line, context)),
+            multi_line: Some(semi_colon_or_comma_to_separator_value(context.config.type_literal_separator_kind_multi_line, context)),
+        },
         prefer_hanging: context.config.type_literal_prefer_hanging,
         prefer_single_line: context.config.type_literal_prefer_single_line,
         surround_single_line_with_spaces: true,
-    }, context)
+    }, context);
+
+    fn semi_colon_or_comma_to_separator_value(value: SemiColonsOrCommas, context: &mut Context) -> SeparatorValue {
+        match value {
+            SemiColonsOrCommas::Commas => SeparatorValue::Comma(context.config.type_literal_trailing_commas),
+            SemiColonsOrCommas::SemiColons => SeparatorValue::SemiColon(context.config.semi_colons),
+        }
+    }
 }
 
 /* jsx */
@@ -2527,8 +2529,7 @@ fn parse_jsx_opening_element<'a>(node: &'a JSXOpeningElement, context: &mut Cont
             prefer_hanging: context.config.jsx_attributes_prefer_hanging,
             force_use_new_lines,
             allow_blank_lines: false,
-            trailing_commas: None,
-            semi_colons: None,
+            separator: Separator::none(),
             single_line_space_at_start: true,
             single_line_space_at_end: node.self_closing,
             custom_single_line_separator: None,
@@ -2785,8 +2786,7 @@ fn parse_object_pat<'a>(node: &'a ObjectPat, context: &mut Context<'a>) -> Print
     items.extend(parse_object_like_node(ParseObjectLikeNodeOptions {
         node_span_data: node.span,
         members: node.props.iter().map(|x| x.into()).collect(),
-        trailing_commas: Some(get_trailing_commas(node, context)),
-        semi_colons: None,
+        separator: get_trailing_commas(node, context).into(),
         prefer_hanging: context.config.object_pattern_prefer_hanging,
         prefer_single_line: context.config.object_pattern_prefer_single_line,
         surround_single_line_with_spaces: true,
@@ -3483,8 +3483,7 @@ fn parse_switch_stmt<'a>(node: &'a SwitchStmt, context: &mut Context<'a>) -> Pri
             }
             node_helpers::has_separating_blank_line(previous, next, context)
         },
-        trailing_commas: None,
-        semi_colons: None,
+        separator: Separator::none(),
     }, context));
     return items;
 }
@@ -3527,8 +3526,7 @@ fn parse_switch_case<'a>(node: &'a SwitchCase, context: &mut Context<'a>) -> Pri
                 should_use_space: None,
                 should_use_new_line: None,
                 should_use_blank_line: |previous, next, context| node_helpers::has_separating_blank_line(previous, next, context),
-                trailing_commas: None,
-                semi_colons: None,
+                separator: Separator::none(),
             }, context)));
         }
     }
@@ -3671,8 +3669,7 @@ fn parse_var_decl<'a>(node: &'a VarDecl, context: &mut Context<'a>) -> PrintItem
             prefer_hanging: context.config.variable_statement_prefer_hanging,
             force_use_new_lines,
             allow_blank_lines: false,
-            trailing_commas: Some(TrailingCommas::Never),
-            semi_colons: None,
+            separator: TrailingCommas::Never.into(),
             single_line_space_at_start: false,
             single_line_space_at_end: false,
             custom_single_line_separator: None,
@@ -4147,8 +4144,7 @@ fn parse_type_parameters<'a>(node: TypeParamNode<'a>, context: &mut Context<'a>)
         prefer_hanging: context.config.type_parameters_prefer_hanging,
         force_use_new_lines,
         allow_blank_lines: false,
-        trailing_commas: Some(get_trailing_commas(&node_span_data, context)),
-        semi_colons: None,
+        separator: get_trailing_commas(&node_span_data, context).into(),
         single_line_space_at_start: false,
         single_line_space_at_end: false,
         custom_single_line_separator: None,
@@ -4573,8 +4569,7 @@ fn parse_array_like_nodes<'a>(opts: ParseArrayLikeNodesOptions<'a>, context: &mu
             prefer_hanging,
             force_use_new_lines,
             allow_blank_lines: true,
-            trailing_commas: Some(trailing_commas),
-            semi_colons: None,
+            separator: trailing_commas.into(),
             single_line_space_at_start: false,
             single_line_space_at_end: false,
             custom_single_line_separator: None,
@@ -4628,8 +4623,7 @@ struct ParseMemberedBodyOptions<'a, FShouldUseBlankLine> where FShouldUseBlankLi
     start_header_info: Option<Info>,
     brace_position: BracePosition,
     should_use_blank_line: FShouldUseBlankLine,
-    trailing_commas: Option<TrailingCommas>,
-    semi_colons: Option<SemiColons>,
+    separator: Separator,
 }
 
 fn parse_membered_body<'a, FShouldUseBlankLine>(
@@ -4651,8 +4645,7 @@ fn parse_membered_body<'a, FShouldUseBlankLine>(
     }, context));
 
     let should_use_blank_line = opts.should_use_blank_line;
-    let trailing_commas = opts.trailing_commas;
-    let semi_colons = opts.semi_colons;
+    let separator = opts.separator;
 
     items.extend(parse_block(|members, context| {
         parse_statements_or_members(ParseStatementsOrMembersOptions {
@@ -4661,8 +4654,7 @@ fn parse_membered_body<'a, FShouldUseBlankLine>(
             should_use_space: None,
             should_use_new_line: None,
             should_use_blank_line,
-            trailing_commas,
-            semi_colons,
+            separator,
         }, context)
     }, ParseBlockOptions {
         span_data: Some(create_span_data(open_brace_token.lo(), BytePos(close_brace_token_pos.hi().0 + 1))),
@@ -4679,8 +4671,7 @@ fn parse_statements<'a>(inner_span_data: Span, stmts: impl Iterator<Item=Node<'a
         should_use_space: None,
         should_use_new_line: None,
         should_use_blank_line: |previous, next, context| node_helpers::has_separating_blank_line(previous, next, context),
-        trailing_commas: None,
-        semi_colons: None,
+        separator: Separator::none(),
     }, context)
 }
 
@@ -4690,8 +4681,7 @@ struct ParseStatementsOrMembersOptions<'a, FShouldUseBlankLine> where FShouldUse
     should_use_space: Option<Box<dyn Fn(&Node, &Node, &mut Context) -> bool>>, // todo: Remove putting functions on heap by using type parameters?
     should_use_new_line: Option<Box<dyn Fn(&Node, &Node, &mut Context) -> bool>>,
     should_use_blank_line: FShouldUseBlankLine,
-    trailing_commas: Option<TrailingCommas>,
-    semi_colons: Option<SemiColons>,
+    separator: Separator,
 }
 
 fn parse_statements_or_members<'a, FShouldUseBlankLine>(
@@ -4726,14 +4716,11 @@ fn parse_statements_or_members<'a, FShouldUseBlankLine>(
             items.extend(if let Some(print_items) = optional_print_items {
                 print_items
             } else {
-                if let Some(trailing_commas) = opts.trailing_commas {
-                    let parsed_comma = get_parsed_trailing_comma(trailing_commas, i == children_len - 1, &|_| Some(true));
-                    parse_comma_separated_value(Some(node.clone()), parsed_comma, context)
-                } else if let Some(semi_colons) = opts.semi_colons {
-                    let parsed_semi_colon = get_parsed_semi_colon(semi_colons, i == children_len - 1, &|_| Some(true));
-                    parse_node_with_semi_colon(Some(node.clone()), parsed_semi_colon, context)
-                } else {
+                if opts.separator.is_none() {
                     parse_node(node.clone(), context)
+                } else {
+                    let parsed_separator = get_parsed_separator(&opts.separator, i == children_len - 1, &|_| Some(true));
+                    parse_node_with_separator(Some(node.clone()), parsed_separator, context)
                 }
             });
             items.push_info(end_info);
@@ -4818,8 +4805,7 @@ fn parse_parameters_or_arguments<'a, F>(opts: ParseParametersOrArgumentsOptions<
                 prefer_hanging,
                 force_use_new_lines,
                 allow_blank_lines: false,
-                trailing_commas: Some(trailing_commas),
-                semi_colons: None,
+                separator: trailing_commas.into(),
                 single_line_space_at_start: false,
                 single_line_space_at_end: false,
                 custom_single_line_separator: None,
@@ -4964,13 +4950,54 @@ fn parse_close_paren_with_type<'a>(opts: ParseCloseParenWithTypeOptions<'a>, con
     }
 }
 
+#[derive(PartialEq)]
+enum SeparatorValue {
+    SemiColon(SemiColons),
+    Comma(TrailingCommas),
+}
+
+struct Separator {
+    single_line: Option<SeparatorValue>,
+    multi_line: Option<SeparatorValue>,
+}
+
+impl Separator {
+    pub fn none() -> Self {
+        Separator {
+            single_line: None,
+            multi_line: None,
+        }
+    }
+
+    pub fn is_none(&self) -> bool {
+        self.single_line.is_none() && self.multi_line.is_none()
+    }
+}
+
+impl From<SemiColons> for Separator {
+    fn from(value: SemiColons) -> Separator {
+        Separator {
+            single_line: Some(SeparatorValue::SemiColon(value)),
+            multi_line: Some(SeparatorValue::SemiColon(value)),
+        }
+    }
+}
+
+impl From<TrailingCommas> for Separator {
+    fn from(value: TrailingCommas) -> Separator {
+        Separator {
+            single_line: Some(SeparatorValue::Comma(value)),
+            multi_line: Some(SeparatorValue::Comma(value)),
+        }
+    }
+}
+
 struct ParseSeparatedValuesOptions<'a> {
     nodes: Vec<Option<Node<'a>>>,
     prefer_hanging: bool,
     force_use_new_lines: bool,
     allow_blank_lines: bool,
-    trailing_commas: Option<TrailingCommas>,
-    semi_colons: Option<SemiColons>,
+    separator: Separator,
     single_line_space_at_start: bool,
     single_line_space_at_end: bool,
     custom_single_line_separator: Option<PrintItems>,
@@ -4991,8 +5018,7 @@ fn parse_separated_values_with_result<'a>(
     context: &mut Context<'a>
 ) -> ParseSeparatedValuesResult {
     let nodes = opts.nodes;
-    let semi_colons = opts.semi_colons;
-    let trailing_commas = opts.trailing_commas;
+    let separator = opts.separator;
     let indent_width = context.config.indent_width;
     let compute_lines_span = opts.allow_blank_lines; // save time otherwise
     parser_helpers::parse_separated_values(|is_multi_line_or_hanging_ref| {
@@ -5010,18 +5036,15 @@ fn parse_separated_values_with_result<'a>(
                     end_line: x.end_line_with_comments(context)
                 })
             } else { None };
-            let items = parser_helpers::new_line_group(if let Some(trailing_commas) = trailing_commas {
-                let parsed_comma = get_parsed_trailing_comma(trailing_commas, i == nodes_count - 1, &is_multi_line_or_hanging);
-                parse_comma_separated_value(value, parsed_comma, context)
-            } else if let Some(semi_colons) = semi_colons {
-                let parsed_semi_colon = get_parsed_semi_colon(semi_colons, i == nodes_count - 1, &is_multi_line_or_hanging);
-                parse_node_with_semi_colon(value, parsed_semi_colon, context)
-            } else {
+            let items = parser_helpers::new_line_group(if separator.is_none() {
                 if let Some(value) = value {
                     parse_node(value, context)
                 } else {
                     PrintItems::new()
                 }
+            } else {
+                let parsed_separator = get_parsed_separator(&separator, i == nodes_count - 1, &is_multi_line_or_hanging);
+                parse_node_with_separator(value, parsed_separator, context)
             });
             parsed_nodes.push(parser_helpers::ParsedValue {
                 items,
@@ -5045,51 +5068,44 @@ fn parse_separated_values_with_result<'a>(
     })
 }
 
-fn parse_comma_separated_value<'a>(value: Option<Node<'a>>, parsed_comma: PrintItems, context: &mut Context<'a>) -> PrintItems {
+fn parse_node_with_separator<'a>(value: Option<Node<'a>>, parsed_separator: PrintItems, context: &mut Context<'a>) -> PrintItems {
     let mut items = PrintItems::new();
     let comma_token = get_comma_token(&value, context);
 
+    // get the trailing comments after the comma token (if the separator in the file is currently a comma)
+    let parsed_trailing_comments = if let Some(comma_token) = comma_token {
+        parse_trailing_comments(comma_token, context)
+    } else {
+        PrintItems::new()
+    };
+
     if let Some(element) = value {
-        let parsed_comma = parsed_comma.into_rc_path();
+        let parsed_separator = parsed_separator.into_rc_path();
         items.extend(parse_node_with_inner_parse(element, context, move |mut items, _| {
             // this Rc clone is necessary because we can't move the captured parsed_comma out of this closure
-            items.push_optional_path(parsed_comma.clone());
+            items.push_optional_path(parsed_separator.clone());
             items
         }));
     } else {
-        items.extend(parsed_comma);
+        items.extend(parsed_separator);
     }
 
-    // get the trailing comments after the comma token
-    if let Some(comma_token) = comma_token {
-        items.extend(parse_trailing_comments(comma_token, context));
-    }
+    items.extend(parsed_trailing_comments);
 
     return items;
 
     fn get_comma_token<'a>(element: &Option<Node<'a>>, context: &mut Context<'a>) -> Option<&'a TokenAndSpan> {
         if let Some(element) = element {
-            context.token_finder.get_next_token_if_comma(element)
+            match context.token_finder.get_next_token_if_comma(element) {
+                Some(comma) => Some(comma),
+                None => context.token_finder.get_last_comma_token_within(element), // may occur for type literals
+            }
         } else {
-            // todo: handle this
-            None
+            None // not a comma separated node
         }
     }
 }
 
-fn parse_node_with_semi_colon<'a>(value: Option<Node<'a>>, parsed_semi_colon: PrintItems, context: &mut Context<'a>) -> PrintItems {
-    if let Some(element) = value {
-        let parsed_semi_colon = parsed_semi_colon.into_rc_path();
-        parse_node_with_inner_parse(element, context, move |mut items, _| {
-            // this Rc clone is necessary because we can't move the captured parsed_semi_colon out of this closure
-            items.push_optional_path(parsed_semi_colon.clone());
-            items
-        })
-    } else {
-        parsed_semi_colon
-    }
-}
-
 /// Some nodes don't have a TsTypeAnn, but instead a Box<TsType>
 fn parse_type_ann_with_colon_if_exists_for_type<'a>(type_ann: &'a Option<Box<TsType>>, context: &mut Context<'a>) -> PrintItems {
     if let Some(type_ann) = type_ann {
@@ -5263,8 +5279,7 @@ fn parse_extends_or_implements<'a>(opts: ParseExtendsOrImplementsOptions<'a>, co
             prefer_hanging: opts.prefer_hanging,
             force_use_new_lines: false,
             allow_blank_lines: false,
-            trailing_commas: Some(TrailingCommas::Never),
-            semi_colons: None,
+            separator: TrailingCommas::Never.into(),
             single_line_space_at_start: true,
             single_line_space_at_end: false,
             custom_single_line_separator: None,
@@ -5280,8 +5295,7 @@ fn parse_extends_or_implements<'a>(opts: ParseExtendsOrImplementsOptions<'a>, co
 struct ParseObjectLikeNodeOptions<'a> {
     node_span_data: Span,
     members: Vec<Node<'a>>,
-    trailing_commas: Option<TrailingCommas>,
-    semi_colons: Option<SemiColons>,
+    separator: Separator,
     prefer_hanging: bool,
     prefer_single_line: bool,
     surround_single_line_with_spaces: bool,
@@ -5310,8 +5324,7 @@ fn parse_object_like_node<'a>(opts: ParseObjectLikeNodeOptions<'a>, context: &mu
                 prefer_hanging: opts.prefer_hanging,
                 force_use_new_lines: force_multi_line,
                 allow_blank_lines: true,
-                trailing_commas: opts.trailing_commas,
-                semi_colons: opts.semi_colons,
+                separator: opts.separator,
                 single_line_space_at_start: opts.surround_single_line_with_spaces,
                 single_line_space_at_end: opts.surround_single_line_with_spaces,
                 custom_single_line_separator: None,
@@ -5511,8 +5524,7 @@ fn parse_decorators<'a>(decorators: &'a Vec<Decorator>, is_inline: bool, context
         prefer_hanging: false, // would need to think about the design because prefer_hanging causes a hanging indent
         force_use_new_lines,
         allow_blank_lines: false,
-        trailing_commas: None,
-        semi_colons: None,
+        separator: Separator::none(),
         single_line_space_at_start: false,
         single_line_space_at_end: is_inline,
         custom_single_line_separator: None,
@@ -6083,8 +6095,7 @@ fn parse_jsx_children<'a>(opts: ParseJsxChildrenOptions<'a>, context: &mut Conte
                 }
                 return node_helpers::has_separating_blank_line(previous, next, context);
             },
-            trailing_commas: None,
-            semi_colons: None,
+            separator: Separator::none(),
         }, context)));
 
         if has_children {
@@ -6579,6 +6590,29 @@ fn has_any_node_comment_on_different_line(nodes: &Vec<impl Ranged>, context: &mu
 
 /* config helpers */
 
+fn get_parsed_separator(separator: &Separator, is_trailing: bool, is_multi_line: &(impl Fn(&mut ConditionResolverContext) -> Option<bool> + Clone + 'static)) -> PrintItems {
+    debug_assert!(!separator.is_none());
+    // performance optimization
+    return if separator.single_line == separator.multi_line {
+        get_items(&separator.single_line, is_trailing, is_multi_line)
+    } else {
+        if_true_or(
+            "is_multi_line",
+            is_multi_line.clone(),
+            get_items(&separator.multi_line, is_trailing, is_multi_line),
+            get_items(&separator.single_line, is_trailing, is_multi_line),
+        ).into()
+    };
+
+    fn get_items(value: &Option<SeparatorValue>, is_trailing: bool, is_multi_line: &(impl Fn(&mut ConditionResolverContext) -> Option<bool> + Clone + 'static)) -> PrintItems {
+        match value {
+            Some(SeparatorValue::Comma(trailing_comma)) => get_parsed_trailing_comma(*trailing_comma, is_trailing, is_multi_line),
+            Some(SeparatorValue::SemiColon(semi_colons)) => get_parsed_semi_colon(*semi_colons, is_trailing, is_multi_line),
+            None => PrintItems::new(),
+        }
+    }
+}
+
 fn get_parsed_trailing_comma(option: TrailingCommas, is_trailing: bool, is_multi_line: &(impl Fn(&mut ConditionResolverContext) -> Option<bool> + Clone + 'static)) -> PrintItems {
     if !is_trailing { return ",".into(); }
 
diff --git a/src/parsing/tokens.rs b/src/parsing/tokens.rs
index b205879..c8e680a 100644
--- a/src/parsing/tokens.rs
+++ b/src/parsing/tokens.rs
@@ -76,6 +76,10 @@ impl<'a> TokenFinder<'a> {
         self.get_last_token_within(node, |token| token.token == Token::RBrace)
     }
 
+    pub fn get_last_comma_token_within(&mut self, node: &dyn Ranged) -> Option<&'a TokenAndSpan> {
+        self.get_last_token_within(node, |token| token.token == Token::Comma)
+    }
+
     pub fn get_first_open_bracket_token_within(&mut self, node: &dyn Ranged) -> Option<&'a TokenAndSpan> {
         self.get_first_token_within(node, |token| token.token == Token::LBracket)
     }


Test patch:
diff --git a/tests/specs/issues/issue0011.txt b/tests/specs/issues/issue0011.txt
new file mode 100644
index 0000000..4538362
--- /dev/null
+++ b/tests/specs/issues/issue0011.txt
@@ -0,0 +1,16 @@
+~~ semiColons: asi, typeLiteral.separatorKind.singleLine: commas ~~
+== should use commas when single line ==
+type Test = { p: string, u: number };
+
+[expect]
+type Test = { p: string, u: number }
+
+== should not use anything multi-line ==
+type Test = {
+    p: string, u: number };
+
+[expect]
+type Test = {
+    p: string
+    u: number
+}
diff --git a/tests/specs/types/TypeLiteral/TypeLiteral_SeparatorKind_Commas.txt b/tests/specs/types/TypeLiteral/TypeLiteral_SeparatorKind_Commas.txt
new file mode 100644
index 0000000..10152d8
--- /dev/null
+++ b/tests/specs/types/TypeLiteral/TypeLiteral_SeparatorKind_Commas.txt
@@ -0,0 +1,16 @@
+~~ typeLiteral.separatorKind: commas ~~
+== should use commas when single line ==
+type Test = { p: string, u: number };
+
+[expect]
+type Test = { p: string, u: number };
+
+== should use commas when multi-line ==
+type Test = {
+    p: string, u: number };
+
+[expect]
+type Test = {
+    p: string,
+    u: number,
+};
diff --git a/tests/specs/types/TypeLiteral/TypeLiteral_SeparatorKind_CommasSingle_SemiColonsMulti.txt b/tests/specs/types/TypeLiteral/TypeLiteral_SeparatorKind_CommasSingle_SemiColonsMulti.txt
new file mode 100644
index 0000000..50ecbe3
--- /dev/null
+++ b/tests/specs/types/TypeLiteral/TypeLiteral_SeparatorKind_CommasSingle_SemiColonsMulti.txt
@@ -0,0 +1,38 @@
+~~ typeLiteral.separatorKind.singleLine: commas, typeLiteral.separatorKind.multiLine: semiColons, lineWidth: 40 ~~
+== should use commas when single line ==
+type Test = { p: string, u: number };
+
+[expect]
+type Test = { p: string, u: number };
+
+== should use semi-colons when multi-line ==
+type Test = {
+    p: string, u: number };
+
+[expect]
+type Test = {
+    p: string;
+    u: number;
+};
+
+== should use semi-colons going from single line to multi ==
+type Test = { p: string, u: number, test: other };
+
+[expect]
+type Test = {
+    p: string;
+    u: number;
+    test: other;
+};
+
+== should handle comments after commas ==
+type Test = {
+    p: string, // testing
+    u: number // testing
+    };
+
+[expect]
+type Test = {
+    p: string; // testing
+    u: number; // testing
+};
diff --git a/tests/specs/types/TypeLiteral/TypeLiteral_SeparatorKind_SemiColonsSingle_CommasMulti.txt b/tests/specs/types/TypeLiteral/TypeLiteral_SeparatorKind_SemiColonsSingle_CommasMulti.txt
new file mode 100644
index 0000000..64106bf
--- /dev/null
+++ b/tests/specs/types/TypeLiteral/TypeLiteral_SeparatorKind_SemiColonsSingle_CommasMulti.txt
@@ -0,0 +1,38 @@
+~~ typeLiteral.separatorKind.singleLine: semiColons, typeLiteral.separatorKind.multiLine: commas, lineWidth: 40 ~~
+== should use commas when single line ==
+type Test = { p: string, u: number };
+
+[expect]
+type Test = { p: string; u: number };
+
+== should use semi-colons when multi-line ==
+type Test = {
+    p: string, u: number };
+
+[expect]
+type Test = {
+    p: string,
+    u: number,
+};
+
+== should use commas going from single line to multi ==
+type Test = { p: string; u: number; test: other };
+
+[expect]
+type Test = {
+    p: string,
+    u: number,
+    test: other,
+};
+
+== should handle comments after semi-colons ==
+type Test = {
+    p: string; // testing
+    u: number; // testing
+    };
+
+[expect]
+type Test = {
+    p: string, // testing
+    u: number, // testing
+};