3 |
4 | Reasonable ESLint, Prettier, and TypeScript configs for epic web devs
5 |
6 |
7 | This makes assumptions about the way you prefer to develop software and gives you configurations that will actually help you in your development.
8 |
26 |
27 |
28 |
29 |
30 | [![Build Status][build-badge]][build]
31 | [![MIT License][license-badge]][license]
32 | [![Code of Conduct][coc-badge]][coc]
33 |
34 |
35 | ## The problem
36 |
37 | You're a professional, but you're mature enough to know that even professionals
38 | can make mistakes, and you value your time enough to not want to waste time
39 | configuring code quality tools or babysitting them.
40 |
41 | ## This solution
42 |
43 | This is a set of configurations you can use in your web projects to avoid
44 | wasting time.
45 |
46 | ## Decisions
47 |
48 | You can learn about the different decisions made for this project in
49 | [the decision docs](./docs/decisions).
50 |
51 | ## Usage
52 |
53 | Technically you configure everything yourself, but you can use the configs in
54 | this project as a starter for your projects (and in some cases you don't need to
55 | configure anything more than the defaults).
56 |
57 | ### Prettier
58 |
59 | The easiest way to use this config is in your `package.json`:
60 |
61 | ```json
62 | "prettier": "@epic-web/config/prettier"
63 | ```
64 |
65 |
66 | Customizing Prettier
67 |
68 | If you want to customize things, you should probably just copy/paste the
69 | built-in config. But if you really want, you can override it using regular
70 | JavaScript stuff.
71 |
72 | Create a `.prettierrc.js` file in your project root with the following content:
73 |
74 | ```js
75 | import defaultConfig from '@epic-web/config/prettier'
76 |
77 | /** @type {import("prettier").Options} */
78 | export default {
79 | ...defaultConfig,
80 | // .. your overrides here...
81 | }
82 | ```
83 |
84 |
85 |
86 | ### TypeScript
87 |
88 | Create a `tsconfig.json` file in your project root with the following content:
89 |
90 | ```json
91 | {
92 | "extends": ["@epic-web/config/typescript"],
93 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
94 | "compilerOptions": {
95 | "paths": {
96 | "#app/*": ["./app/*"],
97 | "#tests/*": ["./tests/*"]
98 | }
99 | }
100 | }
101 | ```
102 |
103 | Create a `reset.d.ts` file in your project with these contents:
104 |
105 | ```typescript
106 | import '@epic-web/config/reset.d.ts'
107 | ```
108 |
109 |
110 | Customizing TypeScript
111 |
112 | Learn more from
113 | [the TypeScript docs here](https://www.typescriptlang.org/tsconfig/#extends).
114 |
115 |
116 |
117 | ### ESLint
118 |
119 | Create a `eslint.config.js` file in your project root with the following
120 | content:
121 |
122 | ```js
123 | import { config as defaultConfig } from '@epic-web/config/eslint'
124 |
125 | /** @type {import("eslint").Linter.Config[]} */
126 | export default [...defaultConfig]
127 | ```
128 |
129 |
130 | Customizing ESLint
131 |
132 | Learn more from
133 | [the Eslint docs here](https://eslint.org/docs/latest/extend/shareable-configs#overriding-settings-from-shareable-configs).
134 |
135 |
136 |
137 | There are endless rules we could enable. However, we want to keep our
138 | configurations minimal and only enable rules that catch real problems (the kind
139 | that are likely to happen). This keeps our linting faster and reduces the number
140 | of false positives.
141 |
142 | ## License
143 |
144 | MIT
145 |
146 |
147 | [build-badge]: https://img.shields.io/github/actions/workflow/status/epicweb-dev/config/release.yml?branch=main&logo=github&style=flat-square
148 | [build]: https://github.com/epicweb-dev/config/actions?query=workflow%3Arelease
149 | [license-badge]: https://img.shields.io/badge/license-MIT%20License-blue.svg?style=flat-square
150 | [license]: https://github.com/epicweb-dev/config/blob/main/LICENSE
151 | [coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
152 | [coc]: https://kentcdodds.com/conduct
153 |
154 |
155 |
156 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Epic Web Config Docs
2 |
3 | This package is pretty simple and small so the docs can be found in the root.
4 | However, there are decision documents in this folder that you may find
5 | interesting.
6 |
--------------------------------------------------------------------------------
/docs/decisions/000-template.md:
--------------------------------------------------------------------------------
1 | # Title
2 |
3 | Date: YYYY-MM-DD
4 |
5 | Status: proposed | rejected | accepted | deprecated | superseded by
6 | [0005](0005-example.md)
7 |
8 | ## Context
9 |
10 | ## Decision
11 |
12 | ## Consequences
13 |
--------------------------------------------------------------------------------
/docs/decisions/001-reset.md:
--------------------------------------------------------------------------------
1 | # Reset
2 |
3 | Date: 2024-05-25
4 |
5 | Status: accepted
6 |
7 | ## Context
8 |
9 | There are some things I want "fixed" in every TypeScript project. For details
10 | and examples, check
11 | [the docs for `@total-typescript/ts-reset`](https://www.totaltypescript.com/ts-reset).
12 |
13 | In addition to what's available in the `ts-reset` package, I also want to have
14 | css variable support in the `style` prop of React elements.
15 |
16 | I want to handle that automatically in the tsconfig, but the problem is you
17 | can't disable it, so it's all or nothing and there could be situations where you
18 | wouldn't want the reset to be applied.
19 |
20 | ## Decision
21 |
22 | We'll create a `reset.d.ts` file and consumers will have to import it in their
23 | project manually.
24 |
25 | ## Consequences
26 |
27 | It's a bit of extra work, and it's an extra file we have to expose, but it
28 | strikes the best balance.
29 |
--------------------------------------------------------------------------------
/docs/decisions/002-minimal-eslint.md:
--------------------------------------------------------------------------------
1 | # Minimal ESLint
2 |
3 | Date: 2024-05-25
4 |
5 | Status: accepted
6 |
7 | ## Context
8 |
9 | There are endless ESLint rules you can enable for your project (no really,
10 | because you can make custom ones there is no end to them). Each rule you enable
11 | does three things:
12 |
13 | 1. Helps catch potential issues
14 | 2. Slows down running ESLint
15 | 3. Increases the number of annoying false positives
16 |
17 | Two of these things are costs and one is a benefit. As professional developers,
18 | we need to evaluate each rule based on whether that rule's benefit outweighs the
19 | cost.
20 |
21 | We determine this by evaluating the risk of an issue slipping through and the
22 | impact on the user. This is going to be relatively subjective for everyone.
23 |
24 | There are some rules which cover high impact issues, but are so unlikely to
25 | happen in any project that they are not worth including. For example,
26 | `no-compare-neg-zero` is protecting you from a pretty odd behavior, but the
27 | likelihood of it catching a real issue is so low it's not worth including.
28 |
29 | Another thing to consider is for TypeScript files, there are many rules which
30 | are completely redundant. For example, `no-setter-return` is a redundant rule in
31 | a TypeScript project because TypeScript will give a compiler error if you try to
32 | return from a setter.
33 |
34 | ## Decision
35 |
36 | We keep the rule set as minimal as reasonable.
37 |
38 | At the current time, we're probably over-minimal and more rules should probably
39 | be added.
40 |
41 | ## Consequences
42 |
43 | People wanting a more strict ESLint will have to add more rules themselves. This
44 | is very easy to do (especially with ESLint v9's flat config).
45 |
--------------------------------------------------------------------------------
/docs/decisions/003-semver.md:
--------------------------------------------------------------------------------
1 | # Semantic Versioning
2 |
3 | Date: 2024-05-25
4 |
5 | Status: accepted
6 |
7 | ## Context
8 |
9 | When you make a change that could break people's existing code, that should be
10 | treated as a "breaking change" which corresponds to the first number in a semver
11 | version number (called the "major version number").
12 |
13 | For some people "breaking change" means "if it could break their build, it
14 | should be a major version bump." Unfortunately for this project, that means
15 | pretty much every change could be a breaking change. Doing things this way would
16 | not only be annoying as a project maintainer, but also it diminishes the meaning
17 | of a major version bump so if there really were an important major change we
18 | couldn't communicate that effectively.
19 |
20 | Some configurations in this project will affect the coming project's runtime
21 | code (like how TypeScript is configured), but most of it will not (like how
22 | Prettier or ESLint is configured).
23 |
24 | ## Decision
25 |
26 | Instead, in this project, we'll define breaking changes as:
27 |
28 | 1. If you have to change the way you consume the package
29 | 2. If the config changes your project's runtime
30 |
31 | ## Consequences
32 |
33 | This means most version bumps will be patch/minor version bumps. Major version
34 | bumps will happen if we change the name of a file, what the config module
35 | exports, or the minimum version of Node/TypeScript that's supported.
36 |
--------------------------------------------------------------------------------
/docs/decisions/004-types-packages.md:
--------------------------------------------------------------------------------
1 | # Types Packages
2 |
3 | Date: 2024-05-25
4 |
5 | Status: accepted
6 |
7 | ## Context
8 |
9 | Epic Web projects use Node.js and React. It would be really handy if this
10 | project included the types for these packages by default.
11 |
12 | However, doing this means the consumer doesn't get to choose the version of the
13 | types which is a major issue.
14 |
15 | ## Decision
16 |
17 | Don't include the types in dependencies.
18 |
19 | ## Consequences
20 |
21 | Consumers will have to install `@types/` packages themselves.
22 |
--------------------------------------------------------------------------------
/docs/decisions/005-sorting-imports.md:
--------------------------------------------------------------------------------
1 | # Title
2 |
3 | Date: 2024-05-27
4 |
5 | Status: accepted
6 |
7 | ## Context
8 |
9 | Import order matters. It determines the order in which modules will be
10 | evaluated. Most of the time this doesn't make an impact on the user experience.
11 | So the import order normally doesn't actually matter.
12 |
13 | Having a pre-defined way to sort imports can reduce the amount of noise in PRs,
14 | especially when people's editors handle automatic imports differently.
15 |
16 | Having the editor yell at you because the import order is not correct is super
17 | annoying, but having the editor do this automatically is nice. If it's something
18 | you don't even have to think about then it's fine.
19 |
20 | Prettier is often used for formatting. Changing the import order isn't really
21 | formatting though, so even though there is
22 | [a plugin](https://npm.im/prettier-plugin-organize-imports) to make Prettier
23 | format the import order, it has a few limitations, and it's philosophically
24 | counter to the purpose of Prettier because changing the import order technically
25 | affects the semantics of the code.
26 |
27 | ESLint on the other hand can handle this for us automatically and allows us to
28 | customize the order itself a bit better. Additionally, if you have a side effect
29 | import (like `import './foo.js'`), it doesn't enforce the import order.
30 |
31 | ## Decision
32 |
33 | Use the `eslint-plugin-import-x` plugin to sort imports.
34 |
35 | ## Consequences
36 |
37 | People who don't like the sorting will need to disable it either inline or in
38 | their own config.
39 |
--------------------------------------------------------------------------------
/docs/decisions/005-verbatim-module-syntax.md:
--------------------------------------------------------------------------------
1 | # verbatimModuleSyntax
2 |
3 | Date: 2024-05-30
4 |
5 | Status: deprecated
6 |
7 | Deprecation date: 2024-05-31
8 |
9 | ## Deprecation Note
10 |
11 | Turns out in Remix that `verbatimModuleSyntax` will cause issues if you try to
12 | import a `type` from a `.server` file into a non `.server` file. Like what we do
13 | in the Epic Stack for our toast utilities:
14 |
15 | ```tsx
16 | import { useEffect } from 'react'
17 | import { toast as showToast } from 'sonner'
18 | import { type Toast } from '#app/utils/toast.server.ts' // <-- the build is very unhappy about this with verbatimModuleSyntax
19 |
20 | export function useToast(toast?: Toast | null) {
21 | useEffect(() => {
22 | if (toast) {
23 | setTimeout(() => {
24 | showToast[toast.type](toast.title, {
25 | id: toast.id,
26 | description: toast.description,
27 | })
28 | }, 0)
29 | }
30 | }, [toast])
31 | }
32 | ```
33 |
34 | For that reason, this has been removed from the config.
35 |
36 | ## Context
37 |
38 | The best context for this can be gathered by reading
39 | [the TypeScript docs on `verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax).
40 |
41 | The short version of this is that it helps TypeScript (and other compilers that
42 | strip types) to know whether to keep a module import or not.
43 |
44 | The idea is: If the import is only there to import types, then it's removed. If
45 | it imports values then it is not.
46 |
47 | ## Decision
48 |
49 | Because it's more predictable behavior (and recommended by TypeScript) we will
50 | enable this rule.
51 |
52 | ## Consequences
53 |
54 | The only change people should experience with this change is a more consistent
55 | and correct behavior. It's unlikely anyone will notice this change, but if they
56 | do it will probably be because it fixed a bug.
57 |
--------------------------------------------------------------------------------
/docs/decisions/006-arrow-parens.md:
--------------------------------------------------------------------------------
1 | # Arrow Parens
2 |
3 | Date: 2024-06-13
4 |
5 | Status: accepted
6 |
7 | ## Context
8 |
9 | Prettier has a configuration option called `arrowParens` which decides whether
10 | to add parentheses around the arguments of arrow functions. The available
11 | options are:
12 |
13 | - "always" - Add parentheses around the arguments of arrow functions.
14 | - "avoid" - Only add parentheses around the arguments of arrow functions if it
15 | improves readability.
16 |
17 | The "always" option adds parentheses around the arguments of arrow functions,
18 | even if there's only one argument. This can result in unnecessary parentheses in
19 | the code.
20 |
21 | The "avoid" option removes parentheses around the arguments if there is only one
22 | argument (and that one argument is not being destructured or defaulted). This
23 | means that if the argument is a single identifier, it will be printed without
24 | parentheses. However, if the argument is a more complex expression, parentheses
25 | will be added due to syntax requirements.
26 |
27 | Just reading those descriptions demonstrates that the rules around when it's ok
28 | to avoid parentheses are more complicated than the simple rule of: "always have
29 | parentheses".
30 |
31 | Additionally, consider this: if you have a single argument in an arrow function,
32 | you will not have parentheses around it. If you then decide to destructure it,
33 | add an argument, add a type, or add a default value, you will have to add
34 | parentheses.
35 |
36 | We want to avoid the extra work required to refactor code as much as possible.
37 | Additionally, simpler rules are often better. The simple rule of "always have
38 | parentheses" around the arguments of arrow functions is much simpler.
39 |
40 | ## Decision
41 |
42 | Update the Prettier config from "avoid" to "always."
43 |
44 | ## Consequences
45 |
46 | People will need to reformat their code when they update `@epic-web/config`. In
47 | accordance to our [semver policy](./003-semver.md), we will not be treating this
48 | as a major version bump.
49 |
--------------------------------------------------------------------------------
/docs/decisions/007-no-semi.md:
--------------------------------------------------------------------------------
1 | # No Semicolons
2 |
3 | Date: 2024-06-14
4 |
5 | Status: accepted
6 |
7 | ## Context
8 |
9 | First off, I want to call out that by not using semicolons, we are not relying
10 | on
11 | [automatic semicolon insertion](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#automatic_semicolon_insertion).
12 | We have build tools and things that are going to compile our code and minify it
13 | and everything, they'll add the semicolons for us automatically.
14 |
15 | Another issue people have with leaving off semicolons is you can start a line
16 | with a bracket or a parentheses and that can cause problems if the previous line
17 | doesn't have a semicolon. We're not going to have those problems because we use
18 | the
19 | [`no-unexpected-multiline`](https://eslint.org/docs/latest/rules/no-unexpected-multiline)
20 | rule from ESLint (not to mention Prettier makes the code look funny if you try
21 | that). For example, if you were to write something like this:
22 |
23 |
24 | ```js
25 | let firstPerson
26 | const people = [
27 | { id: 1, name: 'Bob', age: 8 },
28 | { id: 2, name: 'Alice', age: 11 },
29 | { id: 3, name: 'Charlie', age: 15 },
30 | { id: 4, name: 'Dave', age: 7 },
31 | { id: 5, name: 'Eve', age: 13 }
32 | ]
33 | [firstPerson] = people
34 | ```
35 |
36 | Prettier would rewrite it to look like this:
37 |
38 | ```js
39 | let firstPerson
40 | const people = ([
41 | { id: 1, name: 'Bob', age: 8 },
42 | { id: 2, name: 'Alice', age: 11 },
43 | { id: 3, name: 'Charlie', age: 15 },
44 | { id: 4, name: 'Dave', age: 7 },
45 | { id: 5, name: 'Eve', age: 13 },
46 | ][firstPerson] = people)
47 | ```
48 |
49 | Which makes it much more obvious something weird is happening. This is just a
50 | non-issue.
51 |
52 | Sure, ok, so the problems aren't really problems. Great. But why turn off
53 | semicolons? Turning off semicolons makes the process of refactoring our code
54 | easier by not having to babysit the semicolons. For example:
55 |
56 |
57 | ```js
58 | const people = [
59 | { id: 1, name: 'Bob', age: 8 },
60 | { id: 2, name: 'Alice', age: 11 },
61 | { id: 3, name: 'Charlie', age: 15 },
62 | { id: 4, name: 'Dave', age: 7 },
63 | { id: 5, name: 'Eve', age: 13 },
64 | ];
65 |
66 | const olderThanTenAges = people
67 | .map((person) => person.age)
68 | .filter((age) => age > 10);
69 | ```
70 |
71 | Notice that the final chained operation has a semicolon. If I decided to do the
72 | filter before the map I have to first remove the semicolon, then move the line.
73 | I call this "semicolon babysitting". However, if I don't have semicolons then I
74 | simply move the line.
75 |
76 | It's small, but it's also just one less thing to worry about when refactoring
77 | code, and it shows up in enough situations like this one and others that it's
78 | enough reason to set `semi` to `false`.
79 |
80 | ## Decision
81 |
82 | Set the Prettier config to `semi: false`.
83 |
84 | ## Consequences
85 |
86 | This is the way the config was from the beginning, so it won't affect existing
87 | users. Anyone who wants to use this config and wants to use semicolons can
88 | override that option.
89 |
--------------------------------------------------------------------------------
/docs/decisions/008-new-ts-eslint-rules.md:
--------------------------------------------------------------------------------
1 | # New TS ESLint Rules
2 |
3 | Date: 2024-07-08
4 |
5 | Status: accepted
6 |
7 | ## Context
8 |
9 | In [./002-minimal-eslint.md](002-minimal-eslint.md), it was stated:
10 |
11 | > At the current time, we're probably over-minimal and more rules should
12 | > probably be added.
13 |
14 | [@onemen](https://github.com/onemen)
15 | [created a discussion](https://github.com/epicweb-dev/config/discussions/7) to
16 | enable more rules for TypeScript. As a result, some rules were enabled.
17 |
18 | ## Decision
19 |
20 | Below is the justification for the rules being enabled:
21 |
22 | - `@typescript-eslint/no-misused-promises` - It's pretty easy to forget to add
23 | `await` to a promise when doing a `if (condition) { ... }` (or similar).
24 | - `@typescript-eslint/no-floating-promises` - It's pretty easy to forget to add
25 | `await` to a promise value. If you don't care about the return value, simply
26 | add `void` like so: `void deleteExpiredSessions()`.
27 |
28 | And here's the justification for those which will not be enabled:
29 |
30 | - `@typescript-eslint/require-await` - sometimes you really do want async
31 | without await to make a function async. TypeScript will ensure it's treated as
32 | an async function by consumers and that's enough for me.
33 | - `@typescript-eslint/prefer-promise-reject-errors` - sometimes you aren't the
34 | one creating the error, and you just want to propagate an error object with an
35 | `unknown` type.
36 | - `@typescript-eslint/only-throw-error` - same reason as above. However, this
37 | rule supports options to allow you to throw `any` and `unknown`.
38 | Unfortunately, in Remix you can throw `Response` objects, and we don't want to
39 | enable this rule for those cases.
40 | - `@typescript-eslint/no-unsafe-declaration-merging` - this is a rare enough
41 | problem (especially if you focus on types over interfaces) that it's not worth
42 | enabling.
43 | - `@typescript-eslint/no-unsafe-enum-comparison` - enums are not recommended or
44 | used in epic projects, so it's not worth enabling.
45 | - `@typescript-eslint/no-unsafe-unary-minus` - this is a rare enough problem
46 | that it's not worth enabling.
47 | - `@typescript-eslint/no-base-to-string` - this doesn't handle when your object
48 | actually does implement `toString` unless you do so with a class which is not
49 | 100% of the time. For example, the timings object in the epic stack uses
50 | `defineProperty` to implement `toString`. It's not high enough risk/impact to
51 | enable.
52 | - `@typescript-eslint/no-non-null-assertion` - normally you should not use `!`
53 | to tell TypeScript to ignore the null case, but you're a responsible adult and
54 | if you're going to do that, the linter shouldn't yell at you about it.
55 | - `@typescript-eslint/restrict-template-expressions` - `toString` is a feature
56 | of many built-in objects and custom ones. It's not worth enabling.
57 | - `@typescript-eslint/no-confusing-void-expression` - what's confusing to one
58 | person isn't necessarily confusing to others. Arrow functions that call
59 | something that returns `void` is not confusing and the types will make sure
60 | you don't mess something up.
61 |
62 | These each protect you from `any` and while it's best to avoid using `any`, it's
63 | not worth having a lint rule yell at you when you do:
64 |
65 | - `@typescript-eslint/no-unsafe-argument`
66 | - `@typescript-eslint/no-unsafe-call`
67 | - `@typescript-eslint/no-unsafe-member-access`
68 | - `@typescript-eslint/no-unsafe-return`
69 | - `@typescript-eslint/no-unsafe-assignment`
70 |
71 | ## Consequences
72 |
73 | It's possible some projects are breaking some of the rules we enable. It's
74 | unlikely that fixing those cases will pose much of a challenge.
75 |
--------------------------------------------------------------------------------
/docs/decisions/009-consistent-filename-casing.md:
--------------------------------------------------------------------------------
1 | # Consistent Filename Casing (TS)
2 |
3 | Date: 2025-05-14
4 |
5 | Status: accepted
6 |
7 | ## Context
8 |
9 | TypeScript follows the case sensitivity rules of the file system it’s running on.
10 | This can be problematic if some developers are working in a case-sensitive file system and others aren’t.
11 | If a file attempts to import fileManager.ts by specifying ./FileManager.ts the file will be found
12 | in a case-insensitive file system, but not on a case-sensitive file system.
13 |
14 | When this option is set, TypeScript will issue an error if a program tries to include a file
15 | by a casing different from the casing on disk.
16 |
17 | ## Decision
18 |
19 | Set [`forceConsistentCasingInFileNames`](https://www.typescriptlang.org/tsconfig/forceConsistentCasingInFileNames.html) to true in Typescript
20 |
21 | ## Consequences
22 |
23 | Ensure seamless workflow between developers with different operating systems.
--------------------------------------------------------------------------------
/docs/decisions/README.md:
--------------------------------------------------------------------------------
1 | # Decisions
2 |
3 | This directory contains all the decisions we've made for this project and serves
4 | as a record for whenever we wonder why certain decisions were made.
5 |
6 | Decisions in here are never final. But these documents should serve as a good
7 | way for someone to come up to speed on them.
8 |
--------------------------------------------------------------------------------
/docs/style-guide.md:
--------------------------------------------------------------------------------
1 | # Epic Programming Style Guide
2 |
3 | This style guide is a collection of recommendations for writing code that is
4 | easy to understand, maintain, and scale.
5 |
6 | It goes hand-in-hand with the
7 | [Epic Programming Principles](https://www.epicweb.dev/principles) and the
8 | [Epic Web Config](https://github.com/epicweb-dev/config).
9 |
10 | This is an opinionated style guide that's most useful for people who:
11 |
12 | 1. Don't have a lot of experience writing code and want some guidance on how to
13 | write code that's easy to understand, maintain, and scale.
14 | 2. Have experience writing code but want a set of standards to align on for
15 | working in a team.
16 |
17 | Much of this is subjective, but most opinions are thought through and based on
18 | years of experience working with large codebases and teams.
19 |
20 | Note: Not every possible formatting opinion is mentioned because they are
21 | handled automatically by [Prettier](https://prettier.io) anyway.
22 |
23 | ## JavaScript
24 |
25 | This section will include TypeScript guidelines as well.
26 |
27 | ### Variables
28 |
29 | #### References
30 |
31 | Use `const` by default. Only use `let` when you need to reassign. Never use
32 | `var`.
33 |
34 | Remember that `const` does not mean "constant" in the sense of "unchangeable".
35 | It means "constant reference". So if the value is an object, you can still
36 | change the properties of the object.
37 |
38 | #### Naming conventions
39 |
40 | Use descriptive, clear names that explain the value's purpose. Avoid
41 | single-letter names except in small loops or reducers where the value is obvious
42 | from context.
43 |
44 | ```tsx
45 | // ✅ Good
46 | const workshopTitle = 'Web App Fundamentals'
47 | const instructorName = 'Kent C. Dodds'
48 | const isEnabled = true
49 | const sum = numbers.reduce((total, n) => total + n, 0)
50 | const names = people.map((p) => p.name)
51 |
52 | // ❌ Avoid
53 | const t = 'Web App Fundamentals'
54 | const n = 'Kent C. Dodds'
55 | const e = true
56 | ```
57 |
58 | Follow [the naming cheatsheet](https://github.com/kettanaito/naming-cheatsheet)
59 | by [Artem Zakharchenko](https://github.com/kettanaito) for more specifics on
60 | naming conventions.
61 |
62 | #### Constants
63 |
64 | For truly constant values used across files, use uppercase with underscores:
65 |
66 | ```tsx
67 | const BASE_URL = 'https://epicweb.dev'
68 | const DEFAULT_PORT = 3000
69 | ```
70 |
71 | ### Objects
72 |
73 | #### Literal syntax
74 |
75 | Use object literal syntax for creating objects. Use property shorthand when the
76 | property name matches the variable name.
77 |
78 | ```tsx
79 | // ✅ Good
80 | const name = 'Kent'
81 | const age = 36
82 | const person = { name, age }
83 |
84 | // ❌ Avoid
85 | const name = 'Kent'
86 | const age = 36
87 | const person = { name: name, age: age }
88 | ```
89 |
90 | #### Computed property names
91 |
92 | Use computed property names when creating objects with dynamic property names.
93 |
94 | ```tsx
95 | // ✅ Good
96 | const key = 'name'
97 | const obj = {
98 | [key]: 'Kent',
99 | }
100 |
101 | // ❌ Avoid
102 | const key = 'name'
103 | const obj = {}
104 | obj[key] = 'Kent'
105 | ```
106 |
107 | #### Method shorthand
108 |
109 | Use object method shorthand:
110 |
111 | ```tsx
112 | // ✅ Good
113 | const obj = {
114 | method() {
115 | // ...
116 | },
117 | async asyncMethod() {
118 | // ...
119 | },
120 | }
121 |
122 | // ❌ Avoid
123 | const obj = {
124 | method: function () {
125 | // ...
126 | },
127 | asyncMethod: async function () {
128 | // ...
129 | },
130 | }
131 | ```
132 |
133 | > **Note**: Ordering of properties is not important (and not specified by the
134 | > spec) and it's not a priority for this style guide either.
135 |
136 | #### Accessors
137 |
138 | Don't use them. When I do this:
139 |
140 | ```ts
141 | console.log(person.name)
142 | person.name = 'Bob'
143 | ```
144 |
145 | All I expect to happen is to get the person's name and pass it to the `log`
146 | function and to set the person's name to `'Bob'`.
147 |
148 | Once you start using property accessors (getters and setters) then those
149 | guarantees are off.
150 |
151 | ```ts
152 | // ✅ Good
153 | const person = {
154 | name: 'Hannah',
155 | }
156 |
157 | // ❌ Avoid
158 | const person = {
159 | get name() {
160 | // haha! Now I can do something more than just return the name! 😈
161 | return this.name
162 | },
163 | set name(value) {
164 | // haha! Now I can do something more than just set the name! 😈
165 | this.name = value
166 | },
167 | }
168 | ```
169 |
170 | This violates the principle of least surprise.
171 |
172 | ### Arrays
173 |
174 | #### Literal syntax
175 |
176 | Use Array literal syntax for creating arrays.
177 |
178 | ```tsx
179 | // ✅ Good
180 | const items = [1, 2, 3]
181 |
182 | // ❌ Avoid
183 | const items = new Array(1, 2, 3)
184 | ```
185 |
186 | #### Filtering falsey values
187 |
188 | Use `.filter(Boolean)` to remove falsey values from an array.
189 |
190 | ```tsx
191 | // ✅ Good
192 | const items = [1, null, 2, undefined, 3]
193 | const filteredItems = items.filter(Boolean)
194 |
195 | // ❌ Avoid
196 | const filteredItems = items.filter(
197 | (item) => item !== null && item !== undefined,
198 | )
199 | ```
200 |
201 | #### Array methods over loops
202 |
203 | Use Array methods over loops when transforming arrays with pure functions. Use
204 | `for` loops when imperative code is necessary. Never use `forEach` because it's
205 | never more readable than a `for` loop and there's not situation where the
206 | `forEach` callback function could be pure and useful. Prefer `for...of` over
207 | `for` loops.
208 |
209 | ```tsx
210 | // ✅ Good
211 | const items = [1, 2, 3]
212 | const doubledItems = items.map((n) => n * 2)
213 |
214 | // ❌ Avoid
215 | const doubledItems = []
216 | for (const n of items) {
217 | doubledItems.push(n * 2)
218 | }
219 | ```
220 |
221 | ```tsx
222 | // ✅ Good
223 | for (const n of items) {
224 | // ...
225 | }
226 |
227 | // ❌ Avoid
228 | for (let i = 0; i < items.length; i++) {
229 | const n = items[i]
230 | // ...
231 | }
232 |
233 | // ❌ Avoid
234 | items.forEach((n) => {
235 | // ...
236 | })
237 | ```
238 |
239 | ```tsx
240 | // ✅ Good
241 | for (const [i, n] of items.entries()) {
242 | console.log(`${n} at index ${i}`)
243 | }
244 |
245 | // ❌ Avoid
246 | for (const n of items) {
247 | const i = items.indexOf(n)
248 | console.log(`${n} at index ${i}`)
249 | }
250 | ```
251 |
252 | #### Favor simple chains over `.reduce`
253 |
254 | Favor simple `.filter` and `.map` chains over complex `.reduce` callbacks unless
255 | performance is an issue.
256 |
257 | ```tsx
258 | // ✅ Good
259 | const items = [1, 2, 3, 4, 5]
260 | const doubledGreaterThanTwoItems = items.filter((n) => n > 2).map((n) => n * 2)
261 |
262 | // ❌ Avoid
263 | const doubledItems = items.reduce((acc, n) => {
264 | acc.push(n * 2)
265 | return acc
266 | }, [])
267 | ```
268 |
269 | #### Spread to copy
270 |
271 | Prefer the spread operator to copy an array:
272 |
273 | ```tsx
274 | // ✅ Good
275 | const itemsCopy = [...items]
276 | const combined = [...array1, ...array2]
277 |
278 | // ❌ Avoid
279 | const itemsCopy = items.slice()
280 | const combined = array1.concat(array2)
281 | ```
282 |
283 | #### Non-mutative array methods
284 |
285 | Prefer non-mutative array methods like `toReversed()`, `toSorted()`, and
286 | `toSpliced()` when available. Otherwise, create a new array. Unless performance
287 | is an issue or the original array is not referenced (as in a chain of method
288 | calls).
289 |
290 | ```tsx
291 | // ✅ Good
292 | const reversedItems = items.toReversed()
293 | const mappedFilteredSortedItems = items
294 | .filter((n) => n > 2)
295 | .map((n) => n * 2)
296 | .sort((a, b) => a - b)
297 |
298 | // ❌ Avoid
299 | const reversedItems = items.reverse()
300 | ```
301 |
302 | #### Use `with`
303 |
304 | Use `with` to create a new object with some properties replaced.
305 |
306 | ```tsx
307 | // ✅ Good
308 | const people = [{ name: 'Kent' }, { name: 'Sarah' }]
309 | const personIndex = 0
310 | const peopleWithKentReplaced = people.with(personIndex, { name: 'John' })
311 |
312 | // ❌ Avoid (mutative)
313 | const peopleWithKentReplaced = [...people]
314 | peopleWithKentReplaced[personIndex] = { name: 'John' }
315 | ```
316 |
317 | #### TypeScript array generic
318 |
319 | Prefer the Array generic syntax over brackets for TypeScript types:
320 |
321 | ```tsx
322 | // ✅ Good
323 | const items: Array = []
324 | function transform(numbers: Array) {}
325 |
326 | // ❌ Avoid
327 | const items: string[] = []
328 | function transform(numbers: number[]) {}
329 | ```
330 |
331 | Learn more about the reasoning behind the Array generic syntax in the
332 | [Array Types in TypeScript](https://tkdodo.eu/blog/array-types-in-type-script)
333 | article by [Dominik Dorfmeister](https://github.com/tkdodo).
334 |
335 | ### Destructuring
336 |
337 | #### Destructure objects and arrays
338 |
339 | Use destructuring to make your code more terse.
340 |
341 | ```tsx
342 | // ✅ Good
343 | const { name, avatar, 𝕏: xHandle } = instructor
344 | const [first, second] = items
345 |
346 | // ❌ Avoid
347 | const name = instructor.name
348 | const avatar = instructor.avatar
349 | const xHandle = instructor.𝕏
350 | ```
351 |
352 | Destructuring multiple levels is fine when formatted properly by a formatter,
353 | but can definitely get out of hand, so use your best judgement. As usual, try
354 | both and choose the one you hate the least.
355 |
356 | ```tsx
357 | // ✅ Good (nesting, but still readable)
358 | const {
359 | name,
360 | avatar,
361 | 𝕏: xHandle,
362 | address: [{ city, state, country }],
363 | } = instructor
364 |
365 | // ❌ Avoid (too much nesting)
366 | const [
367 | {
368 | name,
369 | avatar,
370 | 𝕏: xHandle,
371 | address: [
372 | {
373 | city: {
374 | latitude: firstCityLatitude,
375 | longitude: firstCityLongitude,
376 | label: firstCityLabel,
377 | },
378 | state: { label: firstStateLabel },
379 | country: { label: firstCountryLabel },
380 | },
381 | ],
382 | },
383 | ] = instructor
384 | ```
385 |
386 | ### Strings
387 |
388 | #### Interpolation
389 |
390 | Prefer template literals over string concatenation.
391 |
392 | ```tsx
393 | // ✅ Good
394 | const name = 'Kent'
395 | const greeting = `Hello ${name}`
396 |
397 | // ❌ Avoid
398 | const greeting = 'Hello ' + name
399 | ```
400 |
401 | #### Multi-line strings
402 |
403 | Use template literals for multi-line strings.
404 |
405 | ```tsx
406 | // ✅ Good
407 | const html = `
408 |
'
415 | ```
416 |
417 | ### Functions
418 |
419 | #### Function declarations
420 |
421 | Use function declarations over function expressions. Name your functions
422 | descriptively.
423 |
424 | This is important because it allows the function definition to be hoisted to the
425 | top of the block, which means it's callable anywhere which frees your mind to
426 | think about other things.
427 |
428 | ```tsx
429 | // ✅ Good
430 | function calculateTotal(items: Array) {
431 | return items.reduce((sum, item) => sum + item, 0)
432 | }
433 |
434 | // ❌ Avoid
435 | const calculateTotal = function (items: Array) {
436 | return items.reduce((sum, item) => sum + item, 0)
437 | }
438 |
439 | const calculateTotal = (items: Array) =>
440 | items.reduce((sum, item) => sum + item, 0)
441 | ```
442 |
443 | #### Limit single-use functions
444 |
445 | Limit creating single-use functions. By taking a large function and breaking it
446 | down into many smaller functions, you reduce benefits of type inference and have
447 | to define types for each function and make additional decisions about the number
448 | and format of arguments. Instead, extract logic only when it needs to be reused
449 | or when a portion of the logic is clearly part of a unique concern.
450 |
451 | ```tsx
452 | // ✅ Good
453 | function doStuff() {
454 | // thing 1
455 | // ...
456 | // thing 2
457 | // ...
458 | // thing 3
459 | // ...
460 | // thing N
461 | }
462 |
463 | // ❌ Avoid
464 | function doThing1(param1: string, param2: number) {}
465 | function doThing2(param1: boolean, param2: User) {}
466 | function doThing3(param1: string, param2: Array, param3: User) {}
467 | function doThing4(param1: User) {}
468 |
469 | function doStuff() {
470 | doThing1()
471 | // ...
472 | doThing2()
473 | // ...
474 | doThing3()
475 | // ...
476 | doThing4()
477 | }
478 | ```
479 |
480 | #### Default parameters
481 |
482 | Prefer default parameters over short-circuiting.
483 |
484 | ```tsx
485 | // ✅ Good
486 | function createUser(name: string, role = 'user') {
487 | return { name, role }
488 | }
489 |
490 | // ❌ Avoid
491 | function createUser(name: string, role: string) {
492 | role ??= 'user'
493 | return { name, role }
494 | }
495 | ```
496 |
497 | #### Early return
498 |
499 | Return early to avoid deep nesting. Use guard clauses:
500 |
501 | ```tsx
502 | // ✅ Good
503 | function getMinResolutionValue(resolution: number | undefined) {
504 | if (!resolution) return undefined
505 | if (resolution <= 480) return MinResolution.noLessThan480p
506 | if (resolution <= 540) return MinResolution.noLessThan540p
507 | return MinResolution.noLessThan1080p
508 | }
509 |
510 | // ❌ Avoid
511 | function getMinResolutionValue(resolution: number | undefined) {
512 | if (resolution) {
513 | if (resolution <= 480) {
514 | return MinResolution.noLessThan480p
515 | } else if (resolution <= 540) {
516 | return MinResolution.noLessThan540p
517 | } else {
518 | return MinResolution.noLessThan1080p
519 | }
520 | } else {
521 | return undefined
522 | }
523 | }
524 | ```
525 |
526 | #### Async/await
527 |
528 | Prefer async/await over promise chains:
529 |
530 | ```tsx
531 | // ✅ Good
532 | async function fetchUserData(userId: string) {
533 | const user = await getUser(userId)
534 | const posts = await getUserPosts(user.id)
535 | return { user, posts }
536 | }
537 |
538 | // ✅ Fine, because wrapping in try/catch is annoying
539 | function sendAnalytics(event: string) {
540 | return fetch('/api/analytics', {
541 | method: 'POST',
542 | body: JSON.stringify({ event }),
543 | }).catch(() => null)
544 | }
545 |
546 | // ❌ Avoid
547 | function fetchUserData(userId: string) {
548 | return getUser(userId).then((user) => {
549 | return getUserPosts(user.id).then((posts) => ({ user, posts }))
550 | })
551 | }
552 |
553 | // ❌ Avoid
554 | async function sendAnalytics(event: string) {
555 | try {
556 | return await fetch('/api/analytics', {
557 | method: 'POST',
558 | body: JSON.stringify({ event }),
559 | })
560 | } catch {
561 | // ignore
562 | return null
563 | }
564 | }
565 | ```
566 |
567 | #### Inline Callbacks
568 |
569 | Anonymous inline callbacks should be arrow functions:
570 |
571 | ```tsx
572 | // ✅ Good
573 | const items = [1, 2, 3]
574 | const doubledGreaterThanTwoItems = items.filter((n) => n > 2).map((n) => n * 2)
575 |
576 | // ❌ Avoid
577 | const items = [1, 2, 3]
578 | const doubledGreaterThanTwoItems = items
579 | .filter(function (n) {
580 | return n > 2
581 | })
582 | .map(function (n) {
583 | return n * 2
584 | })
585 | ```
586 |
587 | #### Arrow Parens
588 |
589 | Arrow functions should include parentheses even with a single parameter:
590 |
591 |
592 | ```tsx
593 | // ✅ Good
594 | const items = [1, 2, 3]
595 | const doubledGreaterThanTwoItems = items.filter((n) => n > 2).map((n) => n * 2)
596 |
597 | // ❌ Avoid
598 | const items = [1, 2, 3]
599 | const doubledGreaterThanTwoItems = items.filter(n => n > 2).map(n => n * 2)
600 | ```
601 |
602 | This makes it easier to add/remove parameters without having to futz around with
603 | parentheses.
604 |
605 | ### Modules
606 |
607 | #### File Organization
608 |
609 | In general, files that change together should be located close to each other. In
610 | Breaking a single file into multiple files should be avoided unless absolutely
611 | necessary.
612 |
613 | Specifics around file structure depends on a multitude of factors:
614 |
615 | - Framework conventions
616 | - Project size
617 | - Team size
618 |
619 | Strive to keep the file structure as flat as possible.
620 |
621 | #### Module Exports
622 |
623 | Framework and other tool conventions sometimes require default exports, but
624 | prefer named exports in all other cases.
625 |
626 | ```tsx
627 | // ✅ Good
628 | export function add(a: number, b: number) {
629 | return a + b
630 | }
631 |
632 | export function subtract(a: number, b: number) {
633 | return a - b
634 | }
635 |
636 | // ❌ Avoid
637 | export default function add(a: number, b: number) {
638 | return a + b
639 | }
640 | ```
641 |
642 | #### Barrel Files
643 |
644 | Do **not** use barrel files. If you don't know what they are, good. If you do
645 | and you like them, it's probably because you haven't experienced their issues
646 | just yet, but you will. Just avoid them.
647 |
648 | #### Pure Modules
649 |
650 | In general, strive to keep modules pure (read more about this in
651 | [Pure Modules](https://kentcdodds.com/blog/pure-modules)). This will make your
652 | application start faster and be easier to understand and test.
653 |
654 | ```tsx
655 | // ✅ Good
656 | let serverData
657 | export function init(a: number, b: number) {
658 | const el = document.getElementById('server-data')
659 | const json = el.textContent
660 | serverData = JSON.parse(json)
661 | }
662 |
663 | export function getServerData() {
664 | if (!serverData) throw new Error('Server data not initialized')
665 | return serverData
666 | }
667 |
668 | // ❌ Avoid
669 | let serverData
670 | const el = document.getElementById('server-data')
671 | const json = el.textContent
672 | export const serverData = JSON.parse(json)
673 | ```
674 |
675 | > **Note**: In practice, you can't avoid some modules having side-effects (you
676 | > gotta kick off the app somewhere), but most modules should be pure.
677 |
678 | #### Import Conventions
679 |
680 | Import order has semantic meaning (modules are executed in the order they're
681 | imported), but if you keep most modules pure, then order shouldn't matter. For
682 | this reason, having your imports grouped can make things a bit easier to read.
683 |
684 | ```ts
685 | // Group imports in this order:
686 | import 'node:fs' // Built-in
687 | import 'match-sorter' // external packages
688 | import '#app/components' // Internal absolute imports
689 | import '../other-folder' // Internal relative imports
690 | import './local-file' // Local imports
691 | ```
692 |
693 | #### Type Imports
694 |
695 | Each module imported should have a single import statement:
696 |
697 | ```tsx
698 | // ✅ Good
699 | import { type MatchSorterOptions, matchSorter } from 'match-sorter'
700 |
701 | // ❌ Avoid
702 | import { type MatchSorterOptions } from 'match-sorter'
703 | import { matchSorter } from 'match-sorter'
704 | ```
705 |
706 | #### Import Location
707 |
708 | All static imports are executed at the top of the file so they should appear
709 | there as well to avoid confusion.
710 |
711 | ```tsx
712 | // ✅ Good
713 | import { matchSorter } from 'match-sorter'
714 |
715 | function doStuff() {
716 | // ...
717 | }
718 |
719 | // ❌ Avoid
720 | function doStuff() {
721 | // ...
722 | }
723 |
724 | import { matchSorter } from 'match-sorter'
725 | ```
726 |
727 | #### Export Location
728 |
729 | All exports should be inline with the function/type/etc they are exporting. This
730 | avoids duplication of the export identifier and having to keep it updated when
731 | changing the name of the exported thing.
732 |
733 | ```tsx
734 | // ✅ Good
735 | export function add(a: number, b: number) {
736 | return a + b
737 | }
738 |
739 | // ❌ Avoid
740 | function add(a: number, b: number) {
741 | return a + b
742 | }
743 | export { add }
744 | ```
745 |
746 | #### Module Type
747 |
748 | Use ECMAScript modules for everything. The age of CommonJS is over.
749 |
750 | ✅ Good **package.json**:
751 |
752 | ```json
753 | {
754 | "type": "module"
755 | }
756 | ```
757 |
758 | Use **exports** field in **package.json** to explicitly declare module entry
759 | points.
760 |
761 | ✅ Good **package.json**:
762 |
763 | ```json
764 | {
765 | "exports": {
766 | "./utils": "./src/utils.js"
767 | }
768 | }
769 | ```
770 |
771 | #### Import Aliases
772 |
773 | Use import aliases to avoid long relative paths. Use the standard `imports`
774 | config field in **package.json** to declare import aliases.
775 |
776 | ✅ Good **package.json**:
777 |
778 | ```json
779 | {
780 | "imports": {
781 | "#app/*": "./app/*",
782 | "#tests/*": "./tests/*"
783 | }
784 | }
785 | ```
786 |
787 | ```tsx
788 | import { add } from '#app/utils/math.ts'
789 | ```
790 |
791 | > **Note**: Latest versions of TypeScript support this syntax natively.
792 |
793 | #### Include file extensions
794 |
795 | The ECMAScript module spec requires file extensions to be included in import
796 | paths. Even though TypeScript doesn't require it, always include the file
797 | extension in your imports. An exception to this is when importing a module which
798 | has `exports` defined in its **package.json**.
799 |
800 | ```tsx
801 | // ✅ Good
802 | import { redirect } from 'react-router'
803 | import { add } from './math.ts'
804 |
805 | // ❌ Avoid
806 | import { add } from './math'
807 | ```
808 |
809 | ### Properties
810 |
811 | #### Use dot-notation
812 |
813 | When accessing properties on objects, use dot-notation unless you can't
814 | syntactically (like if it's dynamic or uses special characters).
815 |
816 | ```tsx
817 | const user = { name: 'Brittany', 'data-id': '123' }
818 |
819 | // ✅ Good
820 | const name = user.name
821 | const id = user['data-id']
822 | function getUserProperty(user: User, property: string) {
823 | return user[property]
824 | }
825 |
826 | // ❌ Avoid
827 | const name = user['name']
828 | ```
829 |
830 | ### Comparison Operators & Equality
831 |
832 | #### Triple equals
833 |
834 | Use triple equals (`===` and `!==`) for comparisons. This will ensure you're not
835 | falling prey to type coercion.
836 |
837 | That said, when comparing against `null` or `undefined`, using double equals
838 | (`==` and `!=`) is just fine.
839 |
840 | ```tsx
841 | // ✅ Good
842 | const user = { id: '123' }
843 | if (user.id === '123') {
844 | // ...
845 | }
846 | const a = null
847 | if (a === null) {
848 | // ...
849 | }
850 | if (b != null) {
851 | // ...
852 | }
853 |
854 | // ❌ Avoid
855 | if (a == null) {
856 | // ...
857 | }
858 | if (b !== null && b !== undefined) {
859 | // ...
860 | }
861 | ```
862 |
863 | #### Rely on truthiness
864 |
865 | Rely on truthiness instead of comparison operators.
866 |
867 | ```tsx
868 | // ✅ Good
869 | if (user) {
870 | // ...
871 | }
872 |
873 | // ❌ Avoid
874 | if (user === true) {
875 | // ...
876 | }
877 | ```
878 |
879 | #### Switch statement braces
880 |
881 | Using braces in switch statements is recommended because it helps clarify the
882 | scope of each case and it avoids variable declarations from leaking into other
883 | cases.
884 |
885 | ```tsx
886 | // ✅ Good
887 | switch (action.type) {
888 | case 'add': {
889 | const { amount } = action
890 | add(amount)
891 | break
892 | }
893 | case 'remove': {
894 | const { removal } = action
895 | remove(removal)
896 | break
897 | }
898 | }
899 |
900 | // ❌ Avoid
901 | switch (action.type) {
902 | case 'add':
903 | const { amount } = action
904 | add(amount)
905 | break
906 | case 'remove':
907 | const { removal } = action
908 | remove(removal)
909 | break
910 | }
911 | ```
912 |
913 | #### Avoid unnecessary ternaries
914 |
915 | ```tsx
916 | // ✅ Good
917 | const isAdmin = user.role === 'admin'
918 | const value = input ?? defaultValue
919 |
920 | // ❌ Avoid
921 | const isAdmin = user.role === 'admin' ? true : false
922 | const value = input != null ? input : defaultValue
923 | ```
924 |
925 | ### Blocks
926 |
927 | #### Use braces for multi-line blocks
928 |
929 | Use braces for multi-line blocks even when the block is the body of a single
930 | statement.
931 |
932 | ```tsx
933 | // ✅ Good
934 | if (!user) return
935 | if (user.role === 'admin') {
936 | abilities = ['add', 'remove', 'edit', 'create', 'modify', 'fly', 'sing']
937 | }
938 |
939 | // ❌ Avoid
940 | if (user.role === 'admin')
941 | abilities = ['add', 'remove', 'edit', 'create', 'modify', 'fly', 'sing']
942 | ```
943 |
944 | ### Control Statements
945 |
946 | #### Use statements
947 |
948 | Unless you're using the value of the condition in an expression, prefer using
949 | statements instead of expressions.
950 |
951 | ```tsx
952 | // ✅ Good
953 | if (user) {
954 | makeUserHappy(user)
955 | }
956 |
957 | // ❌ Avoid
958 | user && makeUserHappy(user)
959 | ```
960 |
961 | ### Comments
962 |
963 | #### Use comments to explain "why" not "what"
964 |
965 | Comments should explain why something is done a certain way, not what the code
966 | does. The names you use for variables and functions are "self-documenting" in a
967 | sense that they explain what the code does. But if you're doing something in a
968 | way that's non-obvious, comments can be helpful.
969 |
970 | ```tsx
971 | // ✅ Good
972 | // We need to sanitize lineNumber to prevent malicious use on win32
973 | // via: https://example.com/link-to-issue-or-something
974 | if (lineNumber && !(Number.isInteger(lineNumber) && lineNumber > 0)) {
975 | return { status: 'error', message: 'lineNumber must be a positive integer' }
976 | }
977 |
978 | // ❌ Avoid
979 | // Check if lineNumber is valid
980 | if (lineNumber && !(Number.isInteger(lineNumber) && lineNumber > 0)) {
981 | return { status: 'error', message: 'lineNumber must be a positive integer' }
982 | }
983 | ```
984 |
985 | #### Use TODO comments for future improvements
986 |
987 | Use TODO comments to mark code that needs future attention or improvement.
988 |
989 | ```tsx
990 | // ✅ Good
991 | // TODO: figure out how to send error messages as JSX from here...
992 | function getErrorMessage() {
993 | // ...
994 | }
995 |
996 | // ❌ Avoid
997 | // FIXME: this is broken
998 | function getErrorMessage() {
999 | // ...
1000 | }
1001 | ```
1002 |
1003 | #### Use FIXME comments for immediate problems
1004 |
1005 | Use FIXME comments to mark code that needs immediate attention or improvement.
1006 |
1007 | ```tsx
1008 | // ✅ Good
1009 | // FIXME: this is broken
1010 | function getErrorMessage() {
1011 | // ...
1012 | }
1013 | ```
1014 |
1015 | > **Note**: The linter should lint against FIXME comments, so this is useful if
1016 | > you are testing things out and want to make sure you don't accidentally commit
1017 | > your work in progress.
1018 |
1019 | #### Use @ts-expect-error for TypeScript workarounds
1020 |
1021 | When you need to work around TypeScript limitations (or your own knowledge gaps
1022 | with TypeScript), use `@ts-expect-error` with a comment explaining why.
1023 |
1024 | ```tsx
1025 | // ✅ Good
1026 | // @ts-expect-error no idea why this started being an issue suddenly 🤷♂️
1027 | if (jsxEl.name !== 'EpicVideo') return
1028 |
1029 | // ❌ Avoid
1030 | // @ts-ignore
1031 | if (jsxEl.name !== 'EpicVideo') return
1032 | ```
1033 |
1034 | #### Use JSDoc for public APIs
1035 |
1036 | Use JSDoc comments for documenting public APIs and their types.
1037 |
1038 | ```tsx
1039 | // ✅ Good
1040 | /**
1041 | * This function generates a TOTP code from a configuration
1042 | * and this comment will explain a few things that are important for you to
1043 | * understand if you're using this function
1044 | *
1045 | * @param {OTPConfig} config - The configuration for the TOTP
1046 | * @returns {string} The TOTP code
1047 | */
1048 | export function generateTOTP(config: OTPConfig) {
1049 | // ...
1050 | }
1051 | ```
1052 |
1053 | #### Avoid redundant comments
1054 |
1055 | Don't add comments that just repeat what the code already clearly expresses.
1056 |
1057 | ```tsx
1058 | // ✅ Good
1059 | function calculateTotal(items: Array) {
1060 | return items.reduce((sum, item) => sum + item, 0)
1061 | }
1062 |
1063 | // ❌ Avoid
1064 | // This function calculates the total of all items in the array
1065 | function calculateTotal(items: Array) {
1066 | return items.reduce((sum, item) => sum + item, 0)
1067 | }
1068 | ```
1069 |
1070 | ### Semicolons
1071 |
1072 | #### Don't use unnecessary semicolons
1073 |
1074 | Don't use semicolons. The rules for when you should use semicolons are more
1075 | complicated than the rules for when you must use semicolons. With the right
1076 | eslint rule
1077 | ([`no-unexpected-multiline`](https://eslint.org/docs/latest/rules/no-unexpected-multiline))
1078 | and a formatter that will format your code funny for you if you mess up, you can
1079 | avoid the pitfalls. Read more about this in
1080 | [Semicolons in JavaScript: A preference](https://kentcdodds.com/blog/semicolons-in-javascript-a-preference).
1081 |
1082 |
1083 | ```tsx
1084 | // ✅ Good
1085 | const name = 'Kent'
1086 | const age = 36
1087 | const person = { name, age }
1088 | const getPersonAge = () => person.age
1089 | function getPersonName() {
1090 | return person.name
1091 | }
1092 |
1093 | // ❌ Avoid
1094 | const name = 'Kent';
1095 | const age = 36;
1096 | const person = { name, age };
1097 | const getPersonAge = () => person.age;
1098 | function getPersonName() {
1099 | return person.name
1100 | }
1101 | ```
1102 |
1103 | The only time you need semicolons is when you have a statement that starts with
1104 | `(`, `[`, or `` ` ``. Instances where you do that are few and far between. You
1105 | can prefix that with a `;` if you need to and a code formatter will format your
1106 | code funny if you forget to do so (and the linter rule will bug you about it
1107 | too).
1108 |
1109 | ```tsx
1110 | // ✅ Good
1111 | const name = 'Kent'
1112 | const age = 36
1113 | const person = { name, age }
1114 |
1115 | // The formatter will add semicolons here automatically
1116 | ;(async () => {
1117 | const result = await fetch('/api/user')
1118 | return result.json()
1119 | })()
1120 |
1121 | // ❌ Avoid
1122 | const name = 'Kent'
1123 | const age = 36
1124 | const person = { name, age }
1125 |
1126 | // Don't manually add semicolons
1127 | ;(async () => {
1128 | const result = await fetch('/api/user')
1129 | return result.json()
1130 | })()
1131 | ```
1132 |
1133 | ### Types
1134 |
1135 | #### Type Inference
1136 |
1137 | Let TypeScript do the heavy lifting with type inference when possible:
1138 |
1139 | ```ts
1140 | // ✅ Good
1141 | function add(a: number, b: number) {
1142 | return a + b // TypeScript infers return type as number
1143 | }
1144 |
1145 | // ❌ Avoid
1146 | function add(a: number, b: number): number {
1147 | return a + b
1148 | }
1149 | ```
1150 |
1151 | #### Generics
1152 |
1153 | Use generics to create reusable components and functions. And treat type names
1154 | in generics the same way you treat any other kind of variable or parameter
1155 | (because a generic type is basically a parameter!):
1156 |
1157 | ```tsx
1158 | // ✅ Good
1159 | function createArray(length: number, value: Value): Array {
1160 | return Array(length).fill(value)
1161 | }
1162 |
1163 | // ❌ Avoid
1164 | function createStringArray(length: number, value: string) {
1165 | return Array(length).fill(value)
1166 | }
1167 | ```
1168 |
1169 | #### Type Assertions
1170 |
1171 | Avoid type assertions (`as`) when possible. Instead, use type guards or runtime
1172 | validation.
1173 |
1174 | ```tsx
1175 | // ✅ Good
1176 | function isError(maybeError: unknown): maybeError is Error {
1177 | return (
1178 | maybeError &&
1179 | typeof maybeError === 'object' &&
1180 | 'message' in maybeError &&
1181 | typeof maybeError.message === 'string'
1182 | )
1183 | }
1184 |
1185 | // ❌ Avoid
1186 | const error = caughtError as Error
1187 | ```
1188 |
1189 | #### Type Guards
1190 |
1191 | Use type guards to narrow types and provide runtime type safety. Type guards are
1192 | functions that check if a value is of a specific type. The most common way to
1193 | create a type guard is using a type predicate.
1194 |
1195 | ```tsx
1196 | // ✅ Good - Using type predicate
1197 | function isError(maybeError: unknown): maybeError is Error {
1198 | return (
1199 | maybeError &&
1200 | typeof maybeError === 'object' &&
1201 | 'message' in maybeError &&
1202 | typeof maybeError.message === 'string'
1203 | )
1204 | }
1205 |
1206 | // ✅ Good - Using type predicate with schema validation
1207 | function isApp(app: unknown): app is App {
1208 | return AppSchema.safeParse(app).success
1209 | }
1210 |
1211 | // ✅ Good - Using type predicate with composition
1212 | function isExampleApp(app: unknown): app is ExampleApp {
1213 | return isApp(app) && app.type === 'example'
1214 | }
1215 |
1216 | // ❌ Avoid - Not using type predicate
1217 | function isApp(app: unknown): boolean {
1218 | return typeof app === 'object' && app !== null
1219 | }
1220 | ```
1221 |
1222 | Type predicates use the syntax `parameterName is Type` to tell TypeScript that
1223 | the function checks if the parameter is of the specified type. This allows
1224 | TypeScript to narrow the type in code blocks where the function returns true.
1225 |
1226 | ```tsx
1227 | // Usage example:
1228 | const maybeApp: unknown = getSomeApp()
1229 | if (isExampleApp(maybeApp)) {
1230 | // TypeScript now knows that maybeApp is definitely an ExampleApp
1231 | maybeApp.type // TypeScript knows this is 'example'
1232 | }
1233 | ```
1234 |
1235 | #### Schema Validation
1236 |
1237 | Use schema validation (like Zod) for runtime type checking and type inference
1238 | when working with something that crosses the boundary of your codebase.
1239 |
1240 | ```tsx
1241 | // ✅ Good
1242 | const OAuthData = z.object({
1243 | accessToken: z.string(),
1244 | refreshToken: z.string(),
1245 | expiresAt: z.date(),
1246 | })
1247 | type OAuthData = z.infer
1248 |
1249 | const oauthData = OAuthDataSchema.parse(rawData)
1250 |
1251 | // ❌ Avoid
1252 | type OAuthData = {
1253 | accessToken: string
1254 | refreshToken: string
1255 | expiresAt: Date
1256 | }
1257 | const oauthData = rawData as OAuthData
1258 | ```
1259 |
1260 | #### Unknown Type
1261 |
1262 | Use `unknown` instead of `any` for values of unknown type. This forces you to
1263 | perform type checking before using the value.
1264 |
1265 | ```tsx
1266 | // ✅ Good
1267 | function handleError(error: unknown) {
1268 | if (isError(error)) {
1269 | console.error(error.message)
1270 | } else {
1271 | console.error('An unknown error occurred')
1272 | }
1273 | }
1274 |
1275 | // ❌ Avoid
1276 | function handleError(error: any) {
1277 | console.error(error.message)
1278 | }
1279 | ```
1280 |
1281 | #### Type Coercion
1282 |
1283 | Avoid implicit type coercion. Use explicit type conversion when needed. An
1284 | exception to this is working with truthiness.
1285 |
1286 | ```tsx
1287 | // ✅ Good
1288 | const number = Number(stringValue)
1289 | const string = String(numberValue)
1290 | if (user) {
1291 | // ...
1292 | }
1293 |
1294 | // ❌ Avoid
1295 | const number = +stringValue
1296 | const string = '' + numberValue
1297 | if (Boolean(user)) {
1298 | // ...
1299 | }
1300 | ```
1301 |
1302 | ### Naming Conventions
1303 |
1304 | Learn and follow [Artem's](https://github.com/kettanaito)
1305 | [Naming Cheatsheet](https://github.com/kettanaito/naming-cheatsheet). Here's a
1306 | summary:
1307 |
1308 | ```tsx
1309 | // ✅ Good
1310 | const firstName = 'Kent'
1311 | const friends = ['Kate', 'John']
1312 | const pageCount = 5
1313 | const hasPagination = postCount > 10
1314 | const shouldPaginate = postCount > 10
1315 |
1316 | // ❌ Avoid
1317 | const primerNombre = 'Kent'
1318 | const amis = ['Kate', 'John']
1319 | const page_count = 5
1320 | const isPaginatable = postCount > 10
1321 | const onItmClk = () => {}
1322 | ```
1323 |
1324 | Key principles:
1325 |
1326 | 1. Use English for all names
1327 | 2. Be consistent with naming convention (camelCase, PascalCase, etc.)
1328 | 3. Names should be Short, Intuitive, and Descriptive (S-I-D)
1329 | 4. Avoid contractions and context duplication
1330 | 5. Function names should follow the A/HC/LC pattern:
1331 | - Action (get, set, handle, etc.)
1332 | - High Context (what it operates on)
1333 | - Low Context (optional additional context)
1334 |
1335 | For example: `getUserMessages`, `handleClickOutside`, `shouldDisplayMessage`
1336 |
1337 | ### Events
1338 |
1339 | #### Event Constants
1340 |
1341 | Define event constants using a const object. Use uppercase with underscores for
1342 | event names.
1343 |
1344 | ```tsx
1345 | // ✅ Good
1346 | export const EVENTS = {
1347 | USER_CODE_RECEIVED: 'USER_CODE_RECEIVED',
1348 | AUTH_RESOLVED: 'AUTH_RESOLVED',
1349 | AUTH_REJECTED: 'AUTH_REJECTED',
1350 | } as const
1351 |
1352 | // ❌ Avoid
1353 | export const events = {
1354 | userCodeReceived: 'userCodeReceived',
1355 | authResolved: 'authResolved',
1356 | authRejected: 'authRejected',
1357 | }
1358 | ```
1359 |
1360 | #### Event Types
1361 |
1362 | Use TypeScript to define event types based on the event constants.
1363 |
1364 | ```tsx
1365 | // ✅ Good
1366 | export type EventTypes = keyof typeof EVENTS
1367 |
1368 | // ❌ Avoid
1369 | export type EventTypes =
1370 | | 'USER_CODE_RECEIVED'
1371 | | 'AUTH_RESOLVED'
1372 | | 'AUTH_REJECTED'
1373 | ```
1374 |
1375 | #### Event Schemas
1376 |
1377 | Define Zod schemas for event payloads to ensure type safety and runtime
1378 | validation.
1379 |
1380 | ```tsx
1381 | // ✅ Good
1382 | const CodeReceivedEventSchema = z.object({
1383 | type: z.literal(EVENTS.USER_CODE_RECEIVED),
1384 | code: z.string(),
1385 | url: z.string(),
1386 | })
1387 |
1388 | // ❌ Avoid
1389 | type CodeReceivedEvent = {
1390 | type: 'USER_CODE_RECEIVED'
1391 | code: string
1392 | url: string
1393 | }
1394 | ```
1395 |
1396 | > **Note**: This is primarily useful because in event systems, you're typically
1397 | > crossing a boundary of your codebase (network etc.).
1398 |
1399 | #### Event Cleanup
1400 |
1401 | Always clean up event listeners when they're no longer needed.
1402 |
1403 | ```tsx
1404 | // ✅ Good
1405 | authEmitter.on(EVENTS.USER_CODE_RECEIVED, handleCodeReceived)
1406 | return () => {
1407 | authEmitter.off(EVENTS.USER_CODE_RECEIVED, handleCodeReceived)
1408 | }
1409 |
1410 | // ❌ Avoid
1411 | authEmitter.on(EVENTS.USER_CODE_RECEIVED, handleCodeReceived)
1412 | // No cleanup
1413 | ```
1414 |
1415 | #### Event Error Handling
1416 |
1417 | Make certain to cover error cases and emit events for those.
1418 |
1419 | ```tsx
1420 | // ✅ Good
1421 | try {
1422 | // event handling logic
1423 | } catch (error) {
1424 | authEmitter.emit(EVENTS.AUTH_REJECTED, {
1425 | error: getErrorMessage(error),
1426 | })
1427 | }
1428 |
1429 | // ❌ Avoid
1430 | try {
1431 | // event handling logic
1432 | } catch (error) {
1433 | console.error(error)
1434 | }
1435 | ```
1436 |
1437 | ## React
1438 |
1439 | ### Avoid useEffect
1440 |
1441 | [You Might Not Need `useEffect`](https://react.dev/learn/you-might-not-need-an-effect)
1442 |
1443 | Instead of using `useEffect`, use ref callbacks, event handlers with
1444 | `flushSync`, css, `useSyncExternalStore`, etc.
1445 |
1446 | ```tsx
1447 | // This example was ripped from the docs:
1448 | // ✅ Good
1449 | function ProductPage({ product, addToCart }) {
1450 | function buyProduct() {
1451 | addToCart(product)
1452 | showNotification(`Added ${product.name} to the shopping cart!`)
1453 | }
1454 |
1455 | function handleBuyClick() {
1456 | buyProduct()
1457 | }
1458 |
1459 | function handleCheckoutClick() {
1460 | buyProduct()
1461 | navigateTo('/checkout')
1462 | }
1463 | // ...
1464 | }
1465 |
1466 | useEffect(() => {
1467 | setCount(count + 1)
1468 | }, [count])
1469 |
1470 | // ❌ Avoid
1471 | function ProductPage({ product, addToCart }) {
1472 | useEffect(() => {
1473 | if (product.isInCart) {
1474 | showNotification(`Added ${product.name} to the shopping cart!`)
1475 | }
1476 | }, [product])
1477 |
1478 | function handleBuyClick() {
1479 | addToCart(product)
1480 | }
1481 |
1482 | function handleCheckoutClick() {
1483 | addToCart(product)
1484 | navigateTo('/checkout')
1485 | }
1486 | // ...
1487 | }
1488 | ```
1489 |
1490 | There are a lot more examples in the docs. `useEffect` is not banned or
1491 | anything. There are just better ways to handle most cases.
1492 |
1493 | Here's an example of a situation where `useEffect` is appropriate:
1494 |
1495 | ```tsx
1496 | // ✅ Good
1497 | useEffect(() => {
1498 | const controller = new AbortController()
1499 |
1500 | window.addEventListener(
1501 | 'keydown',
1502 | (event: KeyboardEvent) => {
1503 | if (event.key !== 'Escape') return
1504 |
1505 | // do something based on escape key being pressed
1506 | },
1507 | { signal: controller.signal },
1508 | )
1509 |
1510 | return () => {
1511 | controller.abort()
1512 | }
1513 | }, [])
1514 | ```
1515 |
1516 | ### Don't Sync State, Derive It
1517 |
1518 | [Don't Sync State, Derive It](https://kentcdodds.com/blog/dont-sync-state-derive-it)
1519 |
1520 | ```tsx
1521 | // ✅ Good
1522 | const [count, setCount] = useState(0)
1523 | const isEven = count % 2 === 0
1524 |
1525 | // ❌ Avoid
1526 | const [count, setCount] = useState(0)
1527 | const [isEven, setIsEven] = useState(false)
1528 |
1529 | useEffect(() => {
1530 | setIsEven(count % 2 === 0)
1531 | }, [count])
1532 | ```
1533 |
1534 | ### Do not render falsiness
1535 |
1536 | In JSX, do not render falsy values other than `null`.
1537 |
1538 | ```tsx
1539 | // ✅ Good
1540 |
1541 | {contacts.length ?
You have {contacts.length} contacts
: null}
1542 |
1543 |
1544 | // ❌ Avoid
1545 |
1546 | {contacts.length &&
You have {contacts.length} contacts
}
1547 |
1548 | ```
1549 |
1550 | ### Use ternaries
1551 |
1552 | Use ternaries for simple conditionals. When automatically formatted, they should
1553 | be plenty readable, even on multiple lines. Ternaries are also the only
1554 | conditional in the spec (currently) which are expressions and can be used in
1555 | return statements and other places expressions are used.
1556 |
1557 | ```tsx
1558 | // ✅ Good
1559 | const isAdmin = user.role === 'admin'
1560 | const access = isAdmin ? 'granted' : 'denied'
1561 |
1562 | function App({ user }: { user: User }) {
1563 | return (
1564 |