Repository: prettier/prettier (JavaScript)
Date: 2019-03-02 02:35:22+00:00
Task type: Feature

Issue:
Respect quoted object keys, as unquoting breaks aggressive minifiers like Closure Compiler
Hi, related to the discussions in [#838](https://github.com/prettier/prettier/issues/838#issuecomment-381831221) and [#1103](https://github.com/prettier/prettier/issues/1103#issuecomment-290996915), Prettier uses a `consistent-as-needed` approach for quoting object keys (ref: https://eslint.org/docs/rules/quote-props#options).

This behaviour is a blocker for us using Prettier in my current company (...and we'd really love to use it!) because it strips quoted object keys. This negatively impacts the input to Closure Compiler in our tool chain, which mangles all unquoted keys, and breaks places like http call serialisation where those keys have meaning.

We're unfortunately stuck with CC in our current setup, and hacks like using `{ ['foo']: bar }` or adding `// prettier ignore` in various places have been viewed as muddying the code and/or too much overhead.

I'd love to see Prettier support for something like:

- preserving object quotes, i.e. the approach suggested in [#1103](https://github.com/prettier/prettier/issues/1103#issuecomment-290996915)

- handling quotes in a `consistent` way instead of `consistent-as-needed`, i.e. if it finds at least one quote in an object, it will ensure all keys are quoted

Is this something I could help with? Something like the PR in #1108 would be fine for us. We'd also be totally ok with `consistent` quoting if that's more in-line with Prettier's strongly-opinionated philosophy.

Thanks,


Issue discussion:
cc: @vjeux who asked me in #838 to open a seperate issue for this
It doesn't feel like @vjeux was aware of #1103 when he suggested to open a new issue?
#1103 was closed last year (by @vjeux, actually) - am hoping we're ok to start a fresh discussion here. Let me know if I should close this and try to get that one re-opened though.
Okay, so it sounds like there's actually a reasonable workaround for Closure Compiler with advanced mode: putting brackets around the key: `{['key']: value}`. While it is not optimal, I believe that it is the best way forward for a few reasons:

- Closure Compiler with Advanced Mode is barely used in practice. In all the existence of prettier, you are only the second person to say that they are actually using it.
- The workaround of adding `[]` around keys is not pretty but is not unreasonable either and I would argue that it is more explicit than just having quotes changing the meaning of the key.
- My understanding is that quoted keys should be the exception rather than the norm, so it shouldn't affect most of your codebase (I could be wrong though, if you could get some stats on this from your codebase that would be very helpful).
- If we can get away with not adding an option for this, I believe that it would be better for the future of prettier.

Let me know what your thoughts are.
Hi @vjeux, thanks for engaging with this.

> Closure Compiler with Advanced Mode is barely used in practice. In all the existence of prettier, you are only the second person to say that they are actually using it.

I suspect others will be in the same boat, just suffering silently. Maybe some Googlers can chime in too? It may also affect Uglify's prop mangling in a similar fashion.

> The workaround of adding [] around keys is not pretty but is not unreasonable either and I would argue that it is more explicit than just having quotes changing the meaning of the key.

It actually produces different, far-less-optimal output:

```js
const a = { ['foo']: 1};
const b = { ['bar']: 2};
console.log(a, b);
```

becomes:
```js
var a={},b=(a.foo=1,a),c={},d=(c.bar=2,c);console.log(b,d);
```

...which is nuts. Whereas the output without `[]` is:
```js
console.log({foo:1},{bar:2});
```

[CC playground link](https://closure-compiler.appspot.com/home#code%3D%252F%252F%2520%253D%253DClosureCompiler%253D%253D%250A%252F%252F%2520%2540compilation_level%2520ADVANCED_OPTIMIZATIONS%250A%252F%252F%2520%2540output_file_name%2520default.js%250A%252F%252F%2520%253D%253D%252FClosureCompiler%253D%253D%250A%250A%252F%252F%2520ADD%2520YOUR%2520CODE%2520HERE%250Aconst%2520a%2520%253D%2520%257B%2520%255B'foo'%255D%253A%25201%257D%253B%250Aconst%2520b%2520%253D%2520%257B%2520%255B'bar'%255D%253A%25202%257D%253B%250Aconsole.log(a%252C%2520b)%253B%250A%250A).

It's very hard to justify moving to prettier with that workaround, as the compiled output is worse.

> My understanding is that quoted keys should be the exception rather than the norm, so it shouldn't affect most of your codebase

You are correct in that it's the exception here. 

The fear among the team is that Prettier with its current behaviour reduces safety - which is a hard sell for a formatter! 

- Whereas before string keys would be kept, this time we'd need to make sure the devs do things like include a `// prettier-ignore` above the certain statements going forward, which may be hard to enforce with tooling. 

- Or we'd have to rewrite a bunch of our code to use different structures (e.g. `Map<string, T>`, or `Array<Array<string>>`), which is also hard to justify a refactor for, just for better formatting in a large codebase.

> If we can get away with not adding an option for this, I believe that it would be better for the future of prettier.

I'm torn here. I (and others - there are quite a few related issues + duplicates in the project here) would love to see Prettier preserve quoted keys, or work in a `consistent` way (instead of `consistent-as-needed`). 

I realise that can be viewed as being a subjective preference though. 

For us, it's not about subjectivity - it's that without it, I can't think of a tenable / scalable way to use Prettier here, because we can't move away from CC.

We'd _really_ love to use Prettier, and a bunch of us in the company are trying to come up with ways to make it happen, but there doesn't seem to be an elegant way we can enforce it with our current tooling.
What are our options (on Prettier side)? Don't change the quotes, either always (i.e. stop removing quotes) or with an option.. What else?

```jsx
var a={},b=(a.foo=1,a),c={},d=(c.bar=2,c);console.log(b,d);
```

This is really nuts and someone should probably be raise an issue in CC about that
@duailibe the options on the Prettier side I can think of are:

- Keep string names where they are found (pretty simple, see example implementation in #1108)

- Work in a `consistent` way instead of `consistent-as-needed` (https://eslint.org/docs/rules/quote-props#options), i.e. if Prettier finds at least one quoted key, then quote all keys. 

If we wanted a zero-config approach, the least-surprising behaviour to me would be the first.

If we're ok with adding an option, then the second (i.e. a `quoteProps` option to match eslint/tslint) seems the most appealing. At the moment Prettier forces people into the `consistent-as-needed` lint bucket.
@alextreppass:

> It actually produces different, far-less-optimal output:

Seconding @duailibe that this is a bug in the closure compiler; if this bug with them is the only blocker, I don't think prettier should be working around bugs in other tools. Perhaps the best way forward is to raise this with them? Probably worth doing so regardless of how prettier proceeds, honestly.

> Whereas before string keys would be kept, this time we'd need to make sure the devs do things like include a // prettier-ignore above the certain statements going forward, which may be hard to enforce with tooling.

Really? I would expect this to be almost trivial to write an eslint plugin for. A sketch:

```js
module.exports = {
  meta: { /* the usual boilerplate */ },
  create: function(context) {
    return {
      Property: function(node) {
        if (node.computed || node.key == null || node.key.type !== 'Literal') {
          return;
        }
        let comments = context.getSourceCode().getCommentsBefore(node);
        if (!comments.some(c => c.value === ' prettier-ignore')) { // TODO use whatever check prettier does, not this simple matching
          context.report({
            node,
            message: 'String keys should have prettier-ignore comments, for compat with closure compiler',
          });
        }
      },
    };
  }
};
```
Have opened a ticket with the CC team, let's see how that goes: https://github.com/google/closure-compiler/issues/2897

In addition to the Closure/mangler needs here, the aesthetic issue raised in https://github.com/prettier/prettier/issues/1103#issuecomment-330839212 is a real thing - I'm surprised more people haven't run into that!
Once the Closure issue gets fixed, I think we can close this issue as there will be no runtime issues.
Agree, thanks
I don't think we need to leave this open since it's not actionable. Comment if anyone disagrees and we'll reopen.
For what it's worth, within Google we have discussed adopting prettier but (obviously) as the main users of Closure compiler, this bug means we cannot.  I also expect converting all our existing code to put brackets around quoted object properties is a non-starter.

I appreciate the rationale given on these bugs, and it's not your responsibility to solve our problems, but just in case you wanted another use case.  In any case clang-format works well enough for us.
@evmar, can I ask why converting existing code to put brackets around quoted objects is a non-starter? You wouldn't need to do it for all code, just for projects which are trying to adopt prettier, and it can be [done automatically](https://github.com/prettier/prettier/issues/1103#issuecomment-297842553) with little effort. I appreciate wanting to avoid that sort of churn in general, but doing it as part of the process of adopting prettier wouldn't create much additional churn over just introducing prettier in the first place, I'd think.
We already use an automated formatter (clang-format) and it's even required for all TypeScript files, so I would hope rolling out prettier wouldn't change our code much.  You're of course right that it's possible to automate.  I worry much more about how to train everyone who touches JavaScript about this new rule ("use square brackets around quoted keys if the quoting matters for your code").

Property renaming in Closure compiler is already really subtle, and frequently breaks code in a way where the tests pass and it breaks in production, which means it's easy for people to get this wrong and not notice until it's too late.  (In my opinion the design of the renaming feature is pretty wrong, but we have a large quantity of code that relies on it in hard to notice ways.)

> I worry much more about how to train everyone who touches JavaScript about this new rule ("use square brackets around quoted keys if the quoting matters for your code").

@evmar for what it's worth, I wrote an auto-fixing tslint rule for it, and this is caught by a CI step that will fail the build if there are lint errors.
Many (most?) of our users have their editor configured to format on save, or are at least accustomed to running the formatter often. So by the time tslint ran, it might be too late and the quotes might be gone.
@calebegg in our case I made the lint rule auto-fixable (edited my above post to mention it), so on-save it'll become computed.
@evmar Personally, from afar I think the "use computed properties if you want closure not to mangle them" rule is much easier to learn than the "use quoted property names if you want closure not to mangle them" rule you're presumably currently teaching, especially given that there are other reasons to use quoted property names (i.e., non-identifier keys). And then non-mangled keys stand out a lot more: I think that, for myself, I would be much more likely to see `{ ['some-key']: val }` and think to myself "oh, that's not going to get mangled, I should change the key if I want it to be" than I would be for `{ 'some-key': val }`. Especially if my editor changed my `'some-key'` to `['some-key']` on save.

As such, I'd think that long-term it would be better to switch to computed keys anyway, even if it does come with some transitional cost.
The surface of this is more than just object literals, in that e.g. `obj.hasOwnProperty()` takes a string and not a computed property.  So the rule is still about being careful where you use strings, with the addition of a new syntax when those strings show up in object literals.

I appreciate that you don't want to budge on this.  I am just creating a mini-doc internally about why we hadn't adopted prettier and wanted to make sure I covered all the points.  Here, it's not "cannot quote object properties" (which is what the bug title suggests at a glance), but rather "would need to change our JS style, linter, and education around object properties".  It's not surmountable for sure, but also not an obvious improvement.  Thanks for thinking it through with me!
@evmar: I’d love to read that document if you’d be willing to share it. Maybe we can work on finding a middle ground such that you can use prettier at Google
@vjeux The biggest hurdle, and unfortunately one outside of your control, is that clang-format works decently well already and is integrated into lots of our other layers due to it covering C++/Java.  So the main benefit for adopting prettier for us would just be synchronizing with best practices "outside".  This is definitely something we care about, but it's hard to justify the work of doing it.
That makes a lot of sense. I’ve been involved in similar challenges at Facebook.
This singular issue prevents my team from using Prettier. We chose Closure Compiler because it consistently optimizes the best out of any tool in our tests. Without being able to preserve object literals which we painstakingly limit for only certain use cases in our build, Prettier is a non starter. Its a shame because the project is pretty awesome!
It's a shame that people using Google Closure Compiler Advanced Mode can't use prettier. The intention behind having few options is to prevent bike-shedding, not to prevent people from using the project altogether.

Let's discuss and find a solution that would solve this problem. I know that @azz has some ideas.
I'm thinking we can solve this, as well as #838 with a new option:

### `--quote-props`

Values:

`as-needed` (default) - Only add quotes around object properties where required. Current behaviour.
`preserve` - Respect the input (solves this issue)
`consistent` - If _at least one_ property in an object requires quotes, quote all properties (#838)



Solution patch:
diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md
index 2131c474ce8e..a5266f9f45f3 100644
--- a/CHANGELOG.unreleased.md
+++ b/CHANGELOG.unreleased.md
@@ -42,6 +42,44 @@ Examples:
 
 -->
 
+- JavaScript: Add an option to modify when Prettier quotes object properties ([#5934] by [@azz])
+  **`--quote-props <as-needed|preserve|consistent>`**
+
+  `as-needed` **(default)** - Only add quotes around object properties where required. Current behaviour.
+  `preserve` - Respect the input. This is useful for users of Google's Closure Compiler in Advanced Mode, which treats quoted properties differently.
+  `consistent` - If _at least one_ property in an object requires quotes, quote all properties - this is like ESLint's [`consistent-as-needed`](https://eslint.org/docs/rules/quote-props) option.
+
+  <!-- prettier-ignore -->
+  ```js
+  // Input
+  const headers = {
+    accept: "application/json",
+    "content-type": "application/json",
+    "origin": "prettier.io"
+  };
+
+  // Output --quote-props=as-needed
+  const headers = {
+    accept: "application/json",
+    "content-type": "application/json",
+    origin: "prettier.io"
+  };
+
+  // Output --quote-props=consistent
+  const headers = {
+    "accept": "application/json",
+    "content-type": "application/json",
+    "origin": "prettier.io"
+  };
+
+  // Output --quote-props=preserve
+  const headers = {
+    accept: "application/json",
+    "content-type": "application/json",
+    "origin": "prettier.io"
+  };
+  ```
+
 - CLI: Honor stdin-filepath when outputting error messages.
 
 - Markdown: Do not align table contents if it exceeds the print width and `--prose-wrap never` is set ([#5701] by [@chenshuai2144])
diff --git a/docs/options.md b/docs/options.md
index 467cffe86fc2..46450310fb81 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -69,6 +69,20 @@ See the [strings rationale](rationale.md#strings) for more information.
 | ------- | ---------------- | --------------------- |
 | `false` | `--single-quote` | `singleQuote: <bool>` |
 
+## Quote Props
+
+Change when properties in objects are quoted.
+
+Valid options:
+
+- `"as-needed"` - Only add quotes around object properties where required.
+- `"consistent"` - If at least one property in an object requires quotes, quote all properties.
+- `"preserve"` - Respect the input use of quotes in object properties.
+
+| Default       | CLI Override                                                         | API Override                                                         |
+| ------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- |
+| `"as-needed"` | <code>--quote-props <as-needed&#124;consistent&#124;preserve></code> | <code>quoteProps: "<as-needed&#124;consistent&#124;preserve>"</code> |
+
 ## JSX Quotes
 
 Use single quotes instead of double quotes in JSX.
diff --git a/src/language-js/options.js b/src/language-js/options.js
index a20e269f7118..daf365f98d0b 100644
--- a/src/language-js/options.js
+++ b/src/language-js/options.js
@@ -48,6 +48,28 @@ module.exports = {
     default: false,
     description: "Use single quotes in JSX."
   },
+  quoteProps: {
+    since: "1.17.0",
+    category: CATEGORY_JAVASCRIPT,
+    type: "choice",
+    default: "as-needed",
+    description: "Change when properties in objects are quoted.",
+    choices: [
+      {
+        value: "as-needed",
+        description: "Only add quotes around object properties where required."
+      },
+      {
+        value: "consistent",
+        description:
+          "If at least one property in an object requires quotes, quote all properties."
+      },
+      {
+        value: "preserve",
+        description: "Respect the input use of quotes in object properties."
+      }
+    ]
+  },
   trailingComma: {
     since: "0.0.0",
     category: CATEGORY_JAVASCRIPT,
diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js
index 7fd02f191079..e74a13bd1315 100644
--- a/src/language-js/printer-estree.js
+++ b/src/language-js/printer-estree.js
@@ -1,6 +1,7 @@
 "use strict";
 
 const assert = require("assert");
+
 // TODO(azz): anything that imports from main shouldn't be in a `language-*` dir.
 const comments = require("../main/comments");
 const {
@@ -45,6 +46,8 @@ const {
   hasFlowShorthandAnnotationComment
 } = require("./utils");
 
+const needsQuoteProps = new WeakMap();
+
 const {
   builders: {
     concat,
@@ -3679,31 +3682,41 @@ function printStatementSequence(path, options, print) {
 
 function printPropertyKey(path, options, print) {
   const node = path.getNode();
+  const parent = path.getParentNode();
   const key = node.key;
 
+  if (options.quoteProps === "consistent" && !needsQuoteProps.has(parent)) {
+    const objectHasStringProp = (
+      parent.properties ||
+      parent.body ||
+      parent.members
+    ).some(
+      prop =>
+        prop.key &&
+        prop.key.type !== "Identifier" &&
+        !isStringPropSafeToCoerceToIdentifier(prop, options)
+    );
+    needsQuoteProps.set(parent, objectHasStringProp);
+  }
+
   if (
     key.type === "Identifier" &&
     !node.computed &&
-    options.parser === "json"
+    (options.parser === "json" ||
+      (options.quoteProps === "consistent" && needsQuoteProps.get(parent)))
   ) {
     // a -> "a"
+    const prop = printString(JSON.stringify(key.name), options);
     return path.call(
-      keyPath =>
-        comments.printComments(
-          keyPath,
-          () => JSON.stringify(key.name),
-          options
-        ),
+      keyPath => comments.printComments(keyPath, () => prop, options),
       "key"
     );
   }
 
   if (
-    isStringLiteral(key) &&
-    isIdentifierName(key.value) &&
-    !node.computed &&
-    options.parser !== "json" &&
-    !(options.parser === "typescript" && node.type === "ClassProperty")
+    isStringPropSafeToCoerceToIdentifier(node, options) &&
+    (options.quoteProps === "as-needed" ||
+      (options.quoteProps === "consistent" && !needsQuoteProps.get(parent)))
   ) {
     // 'a' -> a
     return path.call(
@@ -6255,6 +6268,16 @@ function isLiteral(node) {
   );
 }
 
+function isStringPropSafeToCoerceToIdentifier(node, options) {
+  return (
+    isStringLiteral(node.key) &&
+    isIdentifierName(node.key.value) &&
+    !node.computed &&
+    options.parser !== "json" &&
+    !(options.parser === "typescript" && node.type === "ClassProperty")
+  );
+}
+
 function isNumericLiteral(node) {
   return (
     node.type === "NumericLiteral" ||
diff --git a/website/playground/Playground.js b/website/playground/Playground.js
index eb0c8939eb81..631287dc1424 100644
--- a/website/playground/Playground.js
+++ b/website/playground/Playground.js
@@ -33,6 +33,7 @@ const ENABLED_OPTIONS = [
   "bracketSpacing",
   "jsxSingleQuote",
   "jsxBracketSameLine",
+  "quoteProps",
   "arrowParens",
   "trailingComma",
   "proseWrap",


Test patch:
diff --git a/tests/quote_props/__snapshots__/jsfmt.spec.js.snap b/tests/quote_props/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..2d21433e6f0d
--- /dev/null
+++ b/tests/quote_props/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,379 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`classes.js 1`] = `
+====================================options=====================================
+parsers: ["flow", "babel"]
+printWidth: 80
+quoteProps: "as-needed"
+                                                                                | printWidth
+=====================================input======================================
+class A {
+  a = "a"
+};
+
+class B {
+  'b' = "b"
+};
+
+class C {
+  c1 = "c1"
+  'c2' = "c2"
+};
+
+class D {
+  d1 = "d1"
+  'd-2' = "d2"
+};
+
+=====================================output=====================================
+class A {
+  a = "a";
+}
+
+class B {
+  b = "b";
+}
+
+class C {
+  c1 = "c1";
+  c2 = "c2";
+}
+
+class D {
+  d1 = "d1";
+  "d-2" = "d2";
+}
+
+================================================================================
+`;
+
+exports[`classes.js 2`] = `
+====================================options=====================================
+parsers: ["flow", "babel"]
+printWidth: 80
+quoteProps: "preserve"
+                                                                                | printWidth
+=====================================input======================================
+class A {
+  a = "a"
+};
+
+class B {
+  'b' = "b"
+};
+
+class C {
+  c1 = "c1"
+  'c2' = "c2"
+};
+
+class D {
+  d1 = "d1"
+  'd-2' = "d2"
+};
+
+=====================================output=====================================
+class A {
+  a = "a";
+}
+
+class B {
+  "b" = "b";
+}
+
+class C {
+  c1 = "c1";
+  "c2" = "c2";
+}
+
+class D {
+  d1 = "d1";
+  "d-2" = "d2";
+}
+
+================================================================================
+`;
+
+exports[`classes.js 3`] = `
+====================================options=====================================
+parsers: ["flow", "babel"]
+printWidth: 80
+quoteProps: "consistent"
+                                                                                | printWidth
+=====================================input======================================
+class A {
+  a = "a"
+};
+
+class B {
+  'b' = "b"
+};
+
+class C {
+  c1 = "c1"
+  'c2' = "c2"
+};
+
+class D {
+  d1 = "d1"
+  'd-2' = "d2"
+};
+
+=====================================output=====================================
+class A {
+  a = "a";
+}
+
+class B {
+  b = "b";
+}
+
+class C {
+  c1 = "c1";
+  c2 = "c2";
+}
+
+class D {
+  "d1" = "d1";
+  "d-2" = "d2";
+}
+
+================================================================================
+`;
+
+exports[`classes.js 4`] = `
+====================================options=====================================
+parsers: ["flow", "babel"]
+printWidth: 80
+quoteProps: "consistent"
+singleQuote: true
+                                                                                | printWidth
+=====================================input======================================
+class A {
+  a = "a"
+};
+
+class B {
+  'b' = "b"
+};
+
+class C {
+  c1 = "c1"
+  'c2' = "c2"
+};
+
+class D {
+  d1 = "d1"
+  'd-2' = "d2"
+};
+
+=====================================output=====================================
+class A {
+  a = 'a';
+}
+
+class B {
+  b = 'b';
+}
+
+class C {
+  c1 = 'c1';
+  c2 = 'c2';
+}
+
+class D {
+  'd1' = 'd1';
+  'd-2' = 'd2';
+}
+
+================================================================================
+`;
+
+exports[`objects.js 1`] = `
+====================================options=====================================
+parsers: ["flow", "babel"]
+printWidth: 80
+quoteProps: "as-needed"
+                                                                                | printWidth
+=====================================input======================================
+const a = {
+  a: "a"
+};
+
+const b = {
+  'b': "b"
+};
+
+const c = {
+  c1: "c1",
+  'c2': "c2"
+};
+
+const d = {
+  d1: "d1",
+  'd-2': "d2"
+};
+
+=====================================output=====================================
+const a = {
+  a: "a"
+};
+
+const b = {
+  b: "b"
+};
+
+const c = {
+  c1: "c1",
+  c2: "c2"
+};
+
+const d = {
+  d1: "d1",
+  "d-2": "d2"
+};
+
+================================================================================
+`;
+
+exports[`objects.js 2`] = `
+====================================options=====================================
+parsers: ["flow", "babel"]
+printWidth: 80
+quoteProps: "preserve"
+                                                                                | printWidth
+=====================================input======================================
+const a = {
+  a: "a"
+};
+
+const b = {
+  'b': "b"
+};
+
+const c = {
+  c1: "c1",
+  'c2': "c2"
+};
+
+const d = {
+  d1: "d1",
+  'd-2': "d2"
+};
+
+=====================================output=====================================
+const a = {
+  a: "a"
+};
+
+const b = {
+  "b": "b"
+};
+
+const c = {
+  c1: "c1",
+  "c2": "c2"
+};
+
+const d = {
+  d1: "d1",
+  "d-2": "d2"
+};
+
+================================================================================
+`;
+
+exports[`objects.js 3`] = `
+====================================options=====================================
+parsers: ["flow", "babel"]
+printWidth: 80
+quoteProps: "consistent"
+                                                                                | printWidth
+=====================================input======================================
+const a = {
+  a: "a"
+};
+
+const b = {
+  'b': "b"
+};
+
+const c = {
+  c1: "c1",
+  'c2': "c2"
+};
+
+const d = {
+  d1: "d1",
+  'd-2': "d2"
+};
+
+=====================================output=====================================
+const a = {
+  a: "a"
+};
+
+const b = {
+  b: "b"
+};
+
+const c = {
+  c1: "c1",
+  c2: "c2"
+};
+
+const d = {
+  "d1": "d1",
+  "d-2": "d2"
+};
+
+================================================================================
+`;
+
+exports[`objects.js 4`] = `
+====================================options=====================================
+parsers: ["flow", "babel"]
+printWidth: 80
+quoteProps: "consistent"
+singleQuote: true
+                                                                                | printWidth
+=====================================input======================================
+const a = {
+  a: "a"
+};
+
+const b = {
+  'b': "b"
+};
+
+const c = {
+  c1: "c1",
+  'c2': "c2"
+};
+
+const d = {
+  d1: "d1",
+  'd-2': "d2"
+};
+
+=====================================output=====================================
+const a = {
+  a: 'a'
+};
+
+const b = {
+  b: 'b'
+};
+
+const c = {
+  c1: 'c1',
+  c2: 'c2'
+};
+
+const d = {
+  'd1': 'd1',
+  'd-2': 'd2'
+};
+
+================================================================================
+`;
diff --git a/tests/quote_props/classes.js b/tests/quote_props/classes.js
new file mode 100644
index 000000000000..117447136f8f
--- /dev/null
+++ b/tests/quote_props/classes.js
@@ -0,0 +1,17 @@
+class A {
+  a = "a"
+};
+
+class B {
+  'b' = "b"
+};
+
+class C {
+  c1 = "c1"
+  'c2' = "c2"
+};
+
+class D {
+  d1 = "d1"
+  'd-2' = "d2"
+};
diff --git a/tests/quote_props/jsfmt.spec.js b/tests/quote_props/jsfmt.spec.js
new file mode 100644
index 000000000000..0c8f1ad3e0d1
--- /dev/null
+++ b/tests/quote_props/jsfmt.spec.js
@@ -0,0 +1,15 @@
+run_spec(__dirname, ["flow", "babel"], {
+  quoteProps: "as-needed"
+});
+
+run_spec(__dirname, ["flow", "babel"], {
+  quoteProps: "preserve"
+});
+
+run_spec(__dirname, ["flow", "babel"], {
+  quoteProps: "consistent"
+});
+run_spec(__dirname, ["flow", "babel"], {
+  quoteProps: "consistent",
+  singleQuote: true
+});
diff --git a/tests/quote_props/objects.js b/tests/quote_props/objects.js
new file mode 100644
index 000000000000..9a3b9be0ad7a
--- /dev/null
+++ b/tests/quote_props/objects.js
@@ -0,0 +1,17 @@
+const a = {
+  a: "a"
+};
+
+const b = {
+  'b': "b"
+};
+
+const c = {
+  c1: "c1",
+  'c2': "c2"
+};
+
+const d = {
+  d1: "d1",
+  'd-2': "d2"
+};
diff --git a/tests/quote_props_typescript/__snapshots__/jsfmt.spec.js.snap b/tests/quote_props_typescript/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000000..0e573f7094f4
--- /dev/null
+++ b/tests/quote_props_typescript/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,94 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`types.ts 1`] = `
+====================================options=====================================
+parsers: ["typescript"]
+printWidth: 80
+quoteProps: "as-needed"
+                                                                                | printWidth
+=====================================input======================================
+type T = {
+  0: string;
+  5: number;
+}
+
+type U = {
+  0: string;
+  "5": number;
+}
+
+=====================================output=====================================
+type T = {
+  0: string;
+  5: number;
+};
+
+type U = {
+  0: string;
+  "5": number;
+};
+
+================================================================================
+`;
+
+exports[`types.ts 2`] = `
+====================================options=====================================
+parsers: ["typescript"]
+printWidth: 80
+quoteProps: "preserve"
+                                                                                | printWidth
+=====================================input======================================
+type T = {
+  0: string;
+  5: number;
+}
+
+type U = {
+  0: string;
+  "5": number;
+}
+
+=====================================output=====================================
+type T = {
+  0: string;
+  5: number;
+};
+
+type U = {
+  0: string;
+  "5": number;
+};
+
+================================================================================
+`;
+
+exports[`types.ts 3`] = `
+====================================options=====================================
+parsers: ["typescript"]
+printWidth: 80
+quoteProps: "consistent"
+                                                                                | printWidth
+=====================================input======================================
+type T = {
+  0: string;
+  5: number;
+}
+
+type U = {
+  0: string;
+  "5": number;
+}
+
+=====================================output=====================================
+type T = {
+  0: string;
+  5: number;
+};
+
+type U = {
+  0: string;
+  "5": number;
+};
+
+================================================================================
+`;
diff --git a/tests/quote_props_typescript/jsfmt.spec.js b/tests/quote_props_typescript/jsfmt.spec.js
new file mode 100644
index 000000000000..b2443baa44f9
--- /dev/null
+++ b/tests/quote_props_typescript/jsfmt.spec.js
@@ -0,0 +1,11 @@
+run_spec(__dirname, ["typescript"], {
+  quoteProps: "as-needed"
+});
+
+run_spec(__dirname, ["typescript"], {
+  quoteProps: "preserve"
+});
+
+run_spec(__dirname, ["typescript"], {
+  quoteProps: "consistent"
+});
diff --git a/tests/quote_props_typescript/types.ts b/tests/quote_props_typescript/types.ts
new file mode 100644
index 000000000000..2d37a43df1e9
--- /dev/null
+++ b/tests/quote_props_typescript/types.ts
@@ -0,0 +1,9 @@
+type T = {
+  0: string;
+  5: number;
+}
+
+type U = {
+  0: string;
+  "5": number;
+}
diff --git a/tests_integration/__tests__/__snapshots__/early-exit.js.snap b/tests_integration/__tests__/__snapshots__/early-exit.js.snap
index 1cb09a2603f4..458a18be656d 100644
--- a/tests_integration/__tests__/__snapshots__/early-exit.js.snap
+++ b/tests_integration/__tests__/__snapshots__/early-exit.js.snap
@@ -79,6 +79,9 @@ Format options:
   --prose-wrap <always|never|preserve>
                            How to wrap prose.
                            Defaults to preserve.
+  --quote-props <as-needed|consistent|preserve>
+                           Change when properties in objects are quoted.
+                           Defaults to as-needed.
   --no-semi                Do not print semicolons, except at the beginning of lines which may need them.
   --single-quote           Use single quotes instead of double quotes.
                            Defaults to false.
@@ -229,6 +232,9 @@ Format options:
   --prose-wrap <always|never|preserve>
                            How to wrap prose.
                            Defaults to preserve.
+  --quote-props <as-needed|consistent|preserve>
+                           Change when properties in objects are quoted.
+                           Defaults to as-needed.
   --no-semi                Do not print semicolons, except at the beginning of lines which may need them.
   --single-quote           Use single quotes instead of double quotes.
                            Defaults to false.
diff --git a/tests_integration/__tests__/__snapshots__/help-options.js.snap b/tests_integration/__tests__/__snapshots__/help-options.js.snap
index 9a0ffd45c1a2..4f43f9fa9f3c 100644
--- a/tests_integration/__tests__/__snapshots__/help-options.js.snap
+++ b/tests_integration/__tests__/__snapshots__/help-options.js.snap
@@ -421,6 +421,25 @@ Default: preserve
 
 exports[`show detailed usage with --help prose-wrap (write) 1`] = `Array []`;
 
+exports[`show detailed usage with --help quote-props (stderr) 1`] = `""`;
+
+exports[`show detailed usage with --help quote-props (stdout) 1`] = `
+"--quote-props <as-needed|consistent|preserve>
+
+  Change when properties in objects are quoted.
+
+Valid options:
+
+  as-needed    Only add quotes around object properties where required.
+  consistent   If at least one property in an object requires quotes, quote all properties.
+  preserve     Respect the input use of quotes in object properties.
+
+Default: as-needed
+"
+`;
+
+exports[`show detailed usage with --help quote-props (write) 1`] = `Array []`;
+
 exports[`show detailed usage with --help range-end (stderr) 1`] = `""`;
 
 exports[`show detailed usage with --help range-end (stdout) 1`] = `
diff --git a/tests_integration/__tests__/__snapshots__/schema.js.snap b/tests_integration/__tests__/__snapshots__/schema.js.snap
index 75a5705dc214..26b422c704a4 100644
--- a/tests_integration/__tests__/__snapshots__/schema.js.snap
+++ b/tests_integration/__tests__/__snapshots__/schema.js.snap
@@ -279,6 +279,30 @@ Multiple values are accepted.",
             },
           ],
         },
+        "quoteProps": Object {
+          "default": "as-needed",
+          "description": "Change when properties in objects are quoted.",
+          "oneOf": Array [
+            Object {
+              "description": "Only add quotes around object properties where required.",
+              "enum": Array [
+                "as-needed",
+              ],
+            },
+            Object {
+              "description": "If at least one property in an object requires quotes, quote all properties.",
+              "enum": Array [
+                "consistent",
+              ],
+            },
+            Object {
+              "description": "Respect the input use of quotes in object properties.",
+              "enum": Array [
+                "preserve",
+              ],
+            },
+          ],
+        },
         "rangeEnd": Object {
           "default": Infinity,
           "description": "Format code ending at a given character offset (exclusive).
diff --git a/tests_integration/__tests__/__snapshots__/support-info.js.snap b/tests_integration/__tests__/__snapshots__/support-info.js.snap
index 96189705d760..0a9974b62e4d 100644
--- a/tests_integration/__tests__/__snapshots__/support-info.js.snap
+++ b/tests_integration/__tests__/__snapshots__/support-info.js.snap
@@ -622,7 +622,27 @@ exports[`API getSupportInfo() with version 1.16.0 -> undefined 1`] = `
         \\"default\\": undefined,
         \\"type\\": \\"choice\\",
       },
-      \\"pluginSearchDirs\\": Object {"
+      \\"pluginSearchDirs\\": Object {
+@@ -165,10 +169,19 @@
+          \\"preserve\\",
+        ],
+        \\"default\\": \\"preserve\\",
+        \\"type\\": \\"choice\\",
+      },
++     \\"quoteProps\\": Object {
++       \\"choices\\": Array [
++         \\"as-needed\\",
++         \\"consistent\\",
++         \\"preserve\\",
++       ],
++       \\"default\\": \\"as-needed\\",
++       \\"type\\": \\"choice\\",
++     },
+      \\"rangeEnd\\": Object {
+        \\"default\\": Infinity,
+        \\"range\\": Object {
+          \\"end\\": Infinity,
+          \\"start\\": 0,"
 `;
 
 exports[`CLI --support-info (stderr) 1`] = `""`;
@@ -1230,6 +1250,29 @@ exports[`CLI --support-info (stdout) 1`] = `
       \\"since\\": \\"1.8.2\\",
       \\"type\\": \\"choice\\"
     },
+    {
+      \\"category\\": \\"JavaScript\\",
+      \\"choices\\": [
+        {
+          \\"description\\": \\"Only add quotes around object properties where required.\\",
+          \\"value\\": \\"as-needed\\"
+        },
+        {
+          \\"description\\": \\"If at least one property in an object requires quotes, quote all properties.\\",
+          \\"value\\": \\"consistent\\"
+        },
+        {
+          \\"description\\": \\"Respect the input use of quotes in object properties.\\",
+          \\"value\\": \\"preserve\\"
+        }
+      ],
+      \\"default\\": \\"as-needed\\",
+      \\"description\\": \\"Change when properties in objects are quoted.\\",
+      \\"name\\": \\"quoteProps\\",
+      \\"pluginDefaults\\": {},
+      \\"since\\": \\"1.17.0\\",
+      \\"type\\": \\"choice\\"
+    },
     {
       \\"category\\": \\"Special\\",
       \\"default\\": null,