From 816ac18cfd41b0152872a7a93a1e5f905e5abc0c Mon Sep 17 00:00:00 2001
From: mkosir
Date: Sat, 14 Dec 2024 06:45:58 +0000
Subject: [PATCH] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@=20mkosir/t?=
=?UTF-8?q?ypescript-style-guide@96bb453092ce15232a8c10633d0e13637b162b40?=
=?UTF-8?q?=20=F0=9F=9A=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
404.html | 2 +-
assets/js/e7ce6630.4fc8f152.js | 1 -
assets/js/e7ce6630.8496596e.js | 1 +
.../{runtime~main.a328bcf4.js => runtime~main.96d60c1f.js} | 2 +-
index.html | 6 +++---
search.html | 2 +-
6 files changed, 7 insertions(+), 7 deletions(-)
delete mode 100644 assets/js/e7ce6630.4fc8f152.js
create mode 100644 assets/js/e7ce6630.8496596e.js
rename assets/js/{runtime~main.a328bcf4.js => runtime~main.96d60c1f.js} (98%)
diff --git a/404.html b/404.html
index 768e31a..0cef423 100644
--- a/404.html
+++ b/404.html
@@ -4,7 +4,7 @@
Page Not Found |
-
+
diff --git a/assets/js/e7ce6630.4fc8f152.js b/assets/js/e7ce6630.4fc8f152.js
deleted file mode 100644
index 4b0b95a..0000000
--- a/assets/js/e7ce6630.4fc8f152.js
+++ /dev/null
@@ -1 +0,0 @@
-"use strict";(self.webpackChunktypescript_style_guide_website=self.webpackChunktypescript_style_guide_website||[]).push([[490],{9558:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>u,contentTitle:()=>h,default:()=>y,frontMatter:()=>p,metadata:()=>t,toc:()=>m});const t=JSON.parse('{"type":"mdx","permalink":"/typescript-style-guide/","source":"@site/src/pages/index.mdx","title":"TypeScript Style Guide","description":"TypeScript Style Guide provides a concise set of conventions and best practices to create consistent, maintainable code.","frontMatter":{"title":"TypeScript Style Guide","description":"TypeScript Style Guide provides a concise set of conventions and best practices to create consistent, maintainable code.","toc_min_heading_level":2,"toc_max_heading_level":2},"unlisted":false}');var i=s(4848),r=s(8453),o=s(9003),a=s(6540);const l=e=>{const n=e.substring(1).replace(/---/g,"__dash__").replace(/--/g," & ").replace(/-/g," ").replace(/__dash__/g," - ");return c(n)},c=e=>e.toLowerCase().split(" ").map((e=>e[0]?.toUpperCase()+e.substring(1))).join(" "),d=e=>{let{children:n}=e;var s;return s=n,(0,a.useEffect)((()=>{const e=()=>{const e=window.location.hash;document.title=e?`${l(e)} | ${s}`:s};return e(),window.addEventListener("popstate",e),()=>{window.removeEventListener("popstate",e)}}),[s]),null},p={title:"TypeScript Style Guide",description:"TypeScript Style Guide provides a concise set of conventions and best practices to create consistent, maintainable code.",toc_min_heading_level:2,toc_max_heading_level:2},h=void 0,u={},m=[{value:"Introduction",id:"introduction",level:2},{value:"Table of Contents",id:"table-of-contents",level:2},{value:"About Guide",id:"about-guide",level:2},{value:"What",id:"what",level:3},{value:"Why",id:"why",level:3},{value:"Disclaimer",id:"disclaimer",level:3},{value:"Requirements",id:"requirements",level:3},{value:"TLDR",id:"tldr",level:2},{value:"Types",id:"types",level:2},{value:"Type Inference",id:"type-inference",level:3},{value:"Data Immutability",id:"data-immutability",level:3},{value:"Required & Optional Object Properties",id:"required--optional-object-properties",level:3},{value:"Discriminated Union",id:"discriminated-union",level:3},{value:"Return Types",id:"return-types",level:3},{value:"Type-Safe Constants with satisfies",id:"type-safe-constants-with-satisfies",level:3},{value:"Template Literal Types",id:"template-literal-types",level:3},{value:"Type any & unknown",id:"type-any--unknown",level:3},{value:"Type & Non-nullability Assertions",id:"type--non-nullability-assertions",level:3},{value:"Type Error",id:"type-error",level:3},{value:"Type Definition",id:"type-definition",level:3},{value:"Array Types",id:"array-types",level:3},{value:"Type Imports and Exports",id:"type-imports-and-exports",level:3},{value:"Services",id:"services",level:3},{value:"Functions",id:"functions",level:2},{value:"General",id:"general",level:3},{value:"Single Object Arg",id:"single-object-arg",level:3},{value:"Required & Optional Args",id:"required--optional-args",level:3},{value:"Args as Discriminated Type",id:"args-as-discriminated-type",level:3},{value:"Variables",id:"variables",level:2},{value:"Const Assertion",id:"const-assertion",level:3},{value:"Enums & Const Assertion",id:"enums--const-assertion",level:3},{value:"Type Union & Boolean Flags",id:"type-union--boolean-flags",level:3},{value:"Null & Undefined",id:"null--undefined",level:3},{value:"Naming",id:"naming",level:2},{value:"Named Export",id:"named-export",level:3},{value:"Naming Conventions",id:"naming-conventions",level:3},{value:"Variables",id:"variables-1",level:4},{value:"Functions",id:"functions-1",level:4},{value:"Types",id:"types-1",level:4},{value:"Generics",id:"generics",level:4},{value:"Abbreviations & Acronyms",id:"abbreviations--acronyms",level:4},{value:"React Components",id:"react-components",level:4},{value:"Prop Types",id:"prop-types",level:4},{value:"Callback Props",id:"callback-props",level:4},{value:"React Hooks",id:"react-hooks",level:4},{value:"Comments",id:"comments",level:3},{value:"Source Organization",id:"source-organization",level:2},{value:"Code Collocation",id:"code-collocation",level:3},{value:"Imports",id:"imports",level:3},{value:"Project Structure",id:"project-structure",level:3},{value:"Appendix - React",id:"appendix---react",level:2},{value:"Required & Optional Props",id:"required--optional-props",level:3},{value:"Props as Discriminated Type",id:"props-as-discriminated-type",level:3},{value:"Props To State",id:"props-to-state",level:3},{value:"Props Type",id:"props-type",level:3},{value:"Component Types",id:"component-types",level:3},{value:"Container",id:"container",level:4},{value:"UI - Feature",id:"ui---feature",level:4},{value:"UI - Design system",id:"ui---design-system",level:4},{value:"Store & Pass Data",id:"store--pass-data",level:3},{value:"Appendix - Tests",id:"appendix---tests",level:2},{value:"What & How To Test",id:"what--how-to-test",level:3},{value:"Test Description",id:"test-description",level:3},{value:"Test Tooling",id:"test-tooling",level:3},{value:"Snapshot",id:"snapshot",level:3}];function x(e){const n={a:"a",br:"br",code:"code",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(d,{children:"TypeScript Style Guide"}),"\n",(0,i.jsx)(o.OB,{children:"TypeScript Style Guide"}),"\n",(0,i.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,i.jsx)(n.p,{children:"TypeScript Style Guide provides a concise set of conventions and best practices to create consistent, maintainable code."}),"\n",(0,i.jsx)(n.h2,{id:"table-of-contents",children:"Table of Contents"}),"\n",(0,i.jsx)(o.MB,{items:m}),"\n",(0,i.jsx)(n.h2,{id:"about-guide",children:"About Guide"}),"\n",(0,i.jsx)(n.h3,{id:"what",children:"What"}),"\n",(0,i.jsxs)(n.p,{children:['Since "consistency is the key", TypeScript Style Guide strives to enforce majority of the rules by using automated tooling as ESLint, TypeScript, Prettier, etc.',(0,i.jsx)(n.br,{}),"\n","Still certain design and architectural decisions must be followed which are described with conventions below."]}),"\n",(0,i.jsx)(n.h3,{id:"why",children:"Why"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"As project grow in size and complexity, maintaining code quality and ensuring consistent practices become increasingly challenging."}),"\n",(0,i.jsx)(n.li,{children:"Defining and following a standard way to write TypeScript applications brings a consistent codebase and faster development cycles."}),"\n",(0,i.jsx)(n.li,{children:"No need to discuss code styles in code reviews."}),"\n",(0,i.jsx)(n.li,{children:"Saves team time and energy."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"disclaimer",children:"Disclaimer"}),"\n",(0,i.jsx)(n.p,{children:"As any code style guide is opinionated, this is no different as it tries to set conventions (sometimes arbitrary) that govern our code."}),"\n",(0,i.jsx)(n.p,{children:"You don't have to follow every convention exactly as written in the guide, decide what works best for your product and team to stay consistent with your codebase."}),"\n",(0,i.jsx)(n.h3,{id:"requirements",children:"Requirements"}),"\n",(0,i.jsx)(n.p,{children:"Style Guide requires you to use:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"https://github.com/microsoft/TypeScript",children:"TypeScript v5"})}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://github.com/typescript-eslint/typescript-eslint",children:"typescript-eslint v7"})," with ",(0,i.jsx)(n.a,{href:"https://typescript-eslint.io/linting/configs/#strict-type-checked",children:(0,i.jsx)(n.code,{children:"strict-type-checked"})})," configuration enabled."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Style Guide assumes using, but is not limited to:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://github.com/facebook/react",children:"React"})," as UI library for frontend conventions."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://playwright.dev/",children:"Playwright"})," and ",(0,i.jsx)(n.a,{href:"https://vitest.dev/",children:"Vitest"})," for testing conventions."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"tldr",children:"TLDR"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Embrace const assertions"}),". ",(0,i.jsx)(n.a,{href:"#const-assertion",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Strive for ",(0,i.jsx)(n.strong,{children:"data immutability"})," using types like ",(0,i.jsx)(n.code,{children:"Readonly"})," and ",(0,i.jsx)(n.code,{children:"ReadonlyArray"}),". ",(0,i.jsx)(n.a,{href:"#data-immutability",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Majority of ",(0,i.jsx)(n.strong,{children:"object properties"})," should be ",(0,i.jsx)(n.strong,{children:"required"})," (use optional sparingly). ",(0,i.jsx)(n.a,{href:"#required--optional-object-properties",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Embrace discriminated unions"}),". ",(0,i.jsx)(n.a,{href:"#discriminated-union",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Avoid type assertions"}),". ",(0,i.jsx)(n.a,{href:"#type--non-nullability-assertions",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Strive for functions to be ",(0,i.jsx)(n.strong,{children:"pure"}),", ",(0,i.jsx)(n.strong,{children:"stateless"})," and have ",(0,i.jsx)(n.strong,{children:"single responsibility"}),". ",(0,i.jsx)(n.a,{href:"#functions",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Strong emphasis to keep ",(0,i.jsx)(n.strong,{children:"naming conventions consistent and readable"}),". ",(0,i.jsx)(n.a,{href:"#naming-conventions",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Use ",(0,i.jsx)(n.strong,{children:"named exports"}),". ",(0,i.jsx)(n.a,{href:"#named-export",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Code is ",(0,i.jsx)(n.strong,{children:"organized"})," and ",(0,i.jsx)(n.strong,{children:"grouped by feature"}),". Collocate code as close as possible to where it's relevant. ",(0,i.jsx)(n.a,{href:"#code-collocation",children:"\u2b63"})]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"types",children:"Types"}),"\n",(0,i.jsxs)(n.p,{children:["When creating types we try to think of how they would best ",(0,i.jsx)(n.strong,{children:"describe our code"}),".",(0,i.jsx)(n.br,{}),"\n","Being expressive and keeping types as ",(0,i.jsx)(n.strong,{children:"narrow as possible"})," brings benefits to the codebase:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Increased Type Safety - Catch errors at compile-time, since narrowed types provide more specific information about the shape and behavior of your data."}),"\n",(0,i.jsx)(n.li,{children:"Improved Code Clarity - Cognitive load is reduced by providing clearer boundaries and constraints on your data which makes your code easier to understand by other developers."}),"\n",(0,i.jsx)(n.li,{children:"Easier Refactoring - Refactor with confidence, since types are narrow, making changes to your code becomes less risky."}),"\n",(0,i.jsx)(n.li,{children:"Optimized Performance - In some cases, narrow types can help the TypeScript compiler generate more optimized JavaScript code."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"type-inference",children:"Type Inference"}),"\n",(0,i.jsx)(n.p,{children:"As rule of thumb, explicitly declare a type when it help narrows it."}),"\n",(0,i.jsx)(o.L7,{children:(0,i.jsx)(n.p,{children:"Just because you don't need to add types, doesn't mean you shouldn't. In some cases explicit type declaration can\nincrease code readability and intent."})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid - Don't explicitly declare a type, it can be inferred.\nconst userRole: string = 'admin'; // Type 'string'\nconst employees = new Map([['Gabriel', 32]]);\nconst [isActive, setIsActive] = useState(false);\n\n// \u2705 Use type inference.\nconst USER_ROLE = 'admin'; // Type 'admin'\nconst employees = new Map([['Gabriel', 32]]); // Type 'Map'\nconst [isActive, setIsActive] = useState(false); // Type 'boolean'\n\n// \u274c Avoid - Don't infer a (wide) type, it can be narrowed.\nconst employees = new Map(); // Type 'Map'\nemployees.set('Lea', 17);\ntype UserRole = 'admin' | 'guest';\nconst [userRole, setUserRole] = useState('admin'); // Type 'string'\n\n// \u2705 Use explicit type declaration to narrow the type.\nconst employees = new Map(); // Type 'Map'\nemployees.set('Gabriel', 32);\ntype UserRole = 'admin' | 'guest';\nconst [userRole, setUserRole] = useState('admin'); // Type 'UserRole'\n"})}),"\n",(0,i.jsx)(n.h3,{id:"data-immutability",children:"Data Immutability"}),"\n",(0,i.jsxs)(n.p,{children:["The majority of the data should be immutable, using types like ",(0,i.jsx)(n.code,{children:"Readonly"})," and ",(0,i.jsx)(n.code,{children:"ReadonlyArray"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Using readonly type prevents accidental data mutations and reduces the risk of bugs caused by unintended side effects."}),"\n",(0,i.jsxs)(n.p,{children:["When performing data processing always return new array, object etc. To minimize cognitive load for future developers, aim to keep data objects flat and small.",(0,i.jsx)(n.br,{}),"\n","Mutations should be used sparingly and only as an exception in cases where they are truly necessary, such as when dealing with complex objects, performance-related reasons etc."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid data mutations\nconst removeFirstUser = (users: Array) => {\n if (users.length === 0) {\n return users;\n }\n return users.splice(1);\n};\n\n// \u2705 Use readonly type to prevent accidental mutations\nconst removeFirstUser = (users: ReadonlyArray) => {\n if (users.length === 0) {\n return users;\n }\n return users.slice(1);\n // Using arr.splice(1) errors - Function 'splice' does not exist on 'users'\n};\n"})}),"\n",(0,i.jsx)(n.h3,{id:"required--optional-object-properties",children:"Required & Optional Object Properties"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Strive to have majority of object properties required and use optional sparingly."})}),"\n",(0,i.jsx)(n.p,{children:"It will reflect designing type-safe and maintainable code:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Clarity and Predictability - Required properties make it explicit which data is always expected. This reduces ambiguity for developers using or consuming the object, as they know exactly what must be present."}),"\n",(0,i.jsx)(n.li,{children:"Type Safety - When properties are required, TypeScript can enforce their presence at compile time. This prevents runtime errors caused by missing properties."}),"\n",(0,i.jsxs)(n.li,{children:["Avoids Overuse of Optional Chaining - If too many properties are optional, it often leads to extensive use of optional chaining (",(0,i.jsx)(n.code,{children:"?."}),") to handle potential undefined values. This clutters the code and obscure its intent."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["If introduction of many optional properties truly can't be avoided utilize ",(0,i.jsx)(n.strong,{children:"discriminated union type"}),"."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid optional properties as they increase complexity\ntype User = {\n id?: number;\n email?: string;\n dashboardAccess?: boolean;\n adminPermissions?: ReadonlyArray;\n subscriptionPlan?: 'free' | 'pro' | 'premium';\n rewardsPoints?: number;\n temporaryToken?: string;\n};\n\n// \u2705 Strive to have majority of properties required, if that's not possible,\n// use discriminated union for clear intent on object usage\ntype AdminUser = {\n role: 'admin';\n id: number;\n email: string;\n dashboardAccess: boolean;\n adminPermissions: ReadonlyArray;\n};\n\ntype RegularUser = {\n role: 'regular';\n id: number;\n email: string;\n subscriptionPlan: 'free' | 'pro' | 'premium';\n rewardsPoints: number;\n};\n\ntype GuestUser = {\n role: 'guest';\n temporaryToken: string;\n};\n\n// Discriminated union type 'User' with no optional properties\ntype User = AdminUser | RegularUser | GuestUser;\n\nconst regularUser: User = {\n role: 'regular',\n id: 212,\n email: 'lea@user.com',\n subscriptionPlan: 'pro',\n rewardsPoints: 1500,\n dashboardAccess: false, // Error 'dashboardAccess' property does not exist\n};\n"})}),"\n",(0,i.jsx)(n.h3,{id:"discriminated-union",children:"Discriminated Union"}),"\n",(0,i.jsx)(n.p,{children:"If there's only one TypeScript feature to choose from, embrace discriminated unions."}),"\n",(0,i.jsxs)(n.p,{children:["Discriminated unions are a powerful concept to model complex data structures and improve type safety, leading to clearer and less error-prone code.",(0,i.jsx)(n.br,{}),"\n","You may encounter discriminated unions under different names such as tagged unions or sum types in various programming languages as C, Haskell, Rust (in conjunction with pattern-matching)."]}),"\n",(0,i.jsx)(n.p,{children:"Discriminated unions advantages:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["As mentioned in ",(0,i.jsx)(n.a,{href:"#required--optional-object-properties",children:"Required & Optional Object Properties"}),", ",(0,i.jsx)(n.a,{href:"#args-as-discriminated-type",children:"Args as Discriminated Union"})," and ",(0,i.jsx)(n.a,{href:"#props-as-discriminated-type",children:"Props as Discriminated Type"}),", discriminated union eliminates optional object properties which decreases complexity."]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Exhaustiveness check - TypeScript can ensure that all possible variants of a type are implemented, eliminating the risk of undefined or unexpected behavior at runtime."}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/switch-exhaustiveness-check/",children:'"@typescript-eslint/switch-exhaustiveness-check": "error"'}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"type Circle = { kind: 'circle'; radius: number };\ntype Square = { kind: 'square'; size: number };\ntype Triangle = { kind: 'triangle'; base: number; height: number };\n\n// Create discriminated union 'Shape', with 'kind' property to discriminate the type of object.\ntype Shape = Circle | Square | Triangle;\n\n// TypeScript warns us with errors in calculateArea function\nconst calculateArea = (shape: Shape) => {\n // Error - Switch is not exhaustive. Cases not matched: \"triangle\"\n switch (shape.kind) {\n case 'circle':\n return Math.PI * shape.radius ** 2;\n case 'square':\n return shape.size * shape.width; // Error - Property 'width' does not exist on type 'square'\n }\n};\n"})}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["Avoid code complexity introduced by ",(0,i.jsx)(n.a,{href:"#type-union--boolean-flags",children:"flag variables"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Clear code intent, as it becomes easier to read and understand by explicitly indicating the possible cases for a given type."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"TypeScript can narrow down union types, ensuring code correctness at compile time."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Discriminated unions make refactoring and maintenance easier by providing a centralized definition of related types. When adding or modifying types within the union, the compiler reports any inconsistencies throughout the codebase."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"IDEs can leverage discriminated unions to provide better autocompletion and type inference."}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"return-types",children:"Return Types"}),"\n",(0,i.jsx)(o.jO,{prefix:"Including return type annotations is highly encouraged, although not required",href:"https://typescript-eslint.io/rules/explicit-function-return-type/",children:'"@typescript-eslint/explicit-function-return-type": "error"'}),"\n",(0,i.jsx)(n.p,{children:"Consider benefits when explicitly typing the return value of a function:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Return values makes it clear and easy to understand to any calling code what type is returned."}),"\n",(0,i.jsx)(n.li,{children:"In the case where there is no return value, the calling code doesn't try to use the undefined value when it shouldn't."}),"\n",(0,i.jsx)(n.li,{children:"Surface potential type errors faster in the future if there are code changes that change the return type of the function."}),"\n",(0,i.jsx)(n.li,{children:"Easier to refactor, since it ensures that the return value is assigned to a variable of the correct type."}),"\n",(0,i.jsx)(n.li,{children:"Similar to writing tests before implementation (TDD), defining function arguments and return type, gives you the opportunity to discuss the feature functionality and its interface ahead of implementation."}),"\n",(0,i.jsx)(n.li,{children:"Although type inference is very convenient, adding return types can save TypeScript compiler a lot of work."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"type-safe-constants-with-satisfies",children:"Type-Safe Constants with satisfies"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"as const satisfies"})," syntax is a powerful TypeScript feature that combines strict type-checking and immutability for constants. It is particularly useful when defining constants that need to conform to a specific type."]}),"\n",(0,i.jsx)(n.p,{children:"Key benefits:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Immutability with ",(0,i.jsx)(n.code,{children:"as const"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Ensures the constant is treated as readonly."}),"\n",(0,i.jsx)(n.li,{children:"Narrows the types of values to their literals, preventing accidental modifications."}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["Validation with ",(0,i.jsx)(n.code,{children:"satisfies"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Ensures the object conforms to a broader type without widening its inferred type."}),"\n",(0,i.jsx)(n.li,{children:"Helps catch type mismatches at compile time while preserving narrowed inferred types."}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// Array constant\ntype UserRole = 'admin' | 'editor' | 'moderator' | 'viewer' | 'guest';\n\n// \u274c Avoid constant of wide type\nconst DASHBOARD_ACCESS_ROLES: ReadonlyArray = ['admin', 'editor', 'moderator'];\n\n// \u274c Avoid constant with incorrect values\nconst DASHBOARD_ACCESS_ROLES = ['admin', 'contributor', 'analyst'] as const;\n\n// \u2705 Use immutable constant of narrowed type\nconst DASHBOARD_ACCESS_ROLES = ['admin', 'editor', 'moderator'] as const satisfies ReadonlyArray;\n\n// Object constant\ntype OrderStatus = {\n pending: 'pending' | 'idle';\n fulfilled: boolean;\n error: string;\n};\n\n// \u274c Avoid mutable constant of wide type\nconst IDLE_ORDER: OrderStatus = {\n pending: 'idle',\n fulfilled: true,\n error: 'Shipping Error',\n};\n\n// \u274c Avoid constant with incorrect values\nconst IDLE_ORDER = {\n pending: 'done',\n fulfilled: 'partially',\n error: 116,\n} as const;\n\n// \u2705 Use immutable constant of narrowed type\nconst IDLE_ORDER = {\n pending: 'idle',\n fulfilled: true,\n error: 'Shipping Error',\n} as const satisfies OrderStatus;\n"})}),"\n",(0,i.jsx)(n.h3,{id:"template-literal-types",children:"Template Literal Types"}),"\n",(0,i.jsxs)(n.p,{children:["Embrace template literal types instead of using the broad ",(0,i.jsx)(n.code,{children:"string"})," type wherever possible.\nTemplate literal types have many practical use cases, such as defining API endpoints, routes, internationalization, database queries, CSS typings etc."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst userEndpoint = '/api/usersss'; // Type 'string' - Typo 'usersss': the route doesn't exist, leading to a runtime error.\n// \u2705 Use\ntype ApiRoute = 'users' | 'posts' | 'comments';\ntype ApiEndpoint = `/api/${ApiRoute}`; // Type ApiEndpoint = \"/api/users\" | \"/api/posts\" | \"/api/comments\"\nconst userEndpoint: ApiEndpoint = '/api/users';\n\n// \u274c Avoid\nconst homeTitle = 'translation.homesss.title'; // Type 'string' - Typo 'homesss': the translation doesn't exist, leading to a runtime error.\n// \u2705 Use\ntype LocaleKeyPages = 'home' | 'about' | 'contact';\ntype TranslationKey = `translation.${LocaleKeyPages}.${string}`; // Type TranslationKey = `translation.home.${string}` | `translation.about.${string}` | `translation.contact.${string}`\nconst homeTitle: TranslationKey = 'translation.home.title';\n\n// \u274c Avoid\nconst color = 'blue-450'; // Type 'string' - Color 'blue-450' doesn't exist, leading to a runtime error.\n// \u2705 Use\ntype BaseColor = 'blue' | 'red' | 'yellow' | 'gray';\ntype Variant = 50 | 100 | 200 | 300 | 400;\ntype Color = `${BaseColor}-${Variant}` | `#${string}`; // Type Color = \"blue-50\" | \"blue-100\" | \"blue-200\" ... | \"red-50\" | \"red-100\" ... | #${string}\nconst iconColor: Color = 'blue-400';\nconst customColor: Color = '#AD3128';\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type-any--unknown",children:"Type any & unknown"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"any"})," data type must not be used as it represents literally \u201cany\u201d value that TypeScript defaults to and skips type checking since it cannot infer the type. As such, ",(0,i.jsx)(n.code,{children:"any"})," is dangerous, it can mask severe programming errors."]}),"\n",(0,i.jsxs)(n.p,{children:["When dealing with ambiguous data type use ",(0,i.jsx)(n.code,{children:"unknown"}),", which is the type-safe counterpart of ",(0,i.jsx)(n.code,{children:"any"}),".",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"unknown"})," doesn't allow dereferencing all properties (anything can be assigned to ",(0,i.jsx)(n.code,{children:"unknown"}),", but ",(0,i.jsx)(n.code,{children:"unknown"})," isn\u2019t assignable to anything)."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid any\nconst foo: any = 'five';\nconst bar: number = foo; // no type error\n\n// \u2705 Use unknown\nconst foo: unknown = 5;\nconst bar: number = foo; // type error - Type 'unknown' is not assignable to type 'number'\n\n// Narrow the type before dereferencing it using:\n// Type guard\nconst isNumber = (num: unknown): num is number => {\n return typeof num === 'number';\n};\nif (!isNumber(foo)) {\n throw Error(`API provided a fault value for field 'foo':${foo}. Should be a number!`);\n}\nconst bar: number = foo;\n\n// Type assertion\nconst bar: number = foo as number;\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type--non-nullability-assertions",children:"Type & Non-nullability Assertions"}),"\n",(0,i.jsxs)(n.p,{children:["Type assertions ",(0,i.jsx)(n.code,{children:"user as User"})," and non-nullability assertions ",(0,i.jsx)(n.code,{children:"user!.name"})," are unsafe. Both only silence TypeScript compiler and increase the risk of crashing application at runtime.",(0,i.jsx)(n.br,{}),"\n","They can only be used as an exception (e.g. third party library types mismatch, dereferencing ",(0,i.jsx)(n.code,{children:"unknown"})," etc.) with a strong rational for why it's introduced into the codebase."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"type User = { id: string; username: string; avatar: string | null };\n// \u274c Avoid type assertions\nconst user = { name: 'Nika' } as User;\n// \u274c Avoid non-nullability assertions\nrenderUserAvatar(user!.avatar); // Runtime error\n\nconst renderUserAvatar = (avatar: string) => {...}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type-error",children:"Type Error"}),"\n",(0,i.jsxs)(n.p,{children:["When a TypeScript error cannot be mitigated, use ",(0,i.jsx)(n.code,{children:"@ts-expect-error"})," as a last resort to suppress it. This directive enables the TypeScript compiler to indicate when the suppressed line no longer contains an error, ensuring that suppressed errors are revisited when they are no longer relevant."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Always use ",(0,i.jsx)(n.code,{children:"@ts-expect-error"})," with a clear description explaining why it is necessary."]}),"\n",(0,i.jsxs)(n.li,{children:["Avoid using ",(0,i.jsx)(n.code,{children:"@ts-ignore"}),", as it does not provide the same level of safety and accountability as @ts-expect-error."]}),"\n"]}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/ban-ts-comment/#allow-with-description",children:"'@typescript-eslint/ban-ts-comment': [\n'error',\n{\n 'ts-expect-error': 'allow-with-description'\n},\n]"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid @ts-ignore as it will do nothing if the following line is error-free.\n// @ts-ignore\nconst newUser = createUser('Gabriel');\n\n// \u2705 Use @ts-expect-error with description.\n// @ts-expect-error: This library function has incorrect type definitions - createUser accepts string as an argument.\nconst newUser = createUser('Gabriel');\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type-definition",children:"Type Definition"}),"\n",(0,i.jsxs)(n.p,{children:["TypeScript offers two options for type definitions - ",(0,i.jsx)(n.code,{children:"type"})," and ",(0,i.jsx)(n.code,{children:"interface"}),". As they come with some functional differences in most cases they can be used interchangeably. We try to limit syntax difference and pick one for consistency."]}),"\n",(0,i.jsx)(o.jO,{prefix:"All types must be defined with `type` alias",href:"https://typescript-eslint.io/rules/consistent-type-definitions/#type",children:"'@typescript-eslint/consistent-type-definitions': ['error', 'type']"}),"\n",(0,i.jsx)(o.L7,{children:(0,i.jsxs)(n.p,{children:["Consider using interfaces when developing a package that may be extended in the future by third-party consumers or\nwhen the team prefers working with interfaces. In such cases, you can disable linting rules where necessary, such as\nwhen using type unions (e.g. ",(0,i.jsx)(n.code,{children:"type Status = 'loading' | 'error'"}),")."]})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid interface definitions\ninterface UserRole = 'admin' | 'guest'; // invalid - interface can't define (commonly used) type unions\n\ninterface UserInfo {\n name: string;\n role: 'admin' | 'guest';\n}\n\n// \u2705 Use type definition\ntype UserRole = 'admin' | 'guest';\n\ntype UserInfo = {\n name: string;\n role: UserRole;\n};\n\n"})}),"\n",(0,i.jsxs)(n.p,{children:["When performing declaration merging (e.g. extending third-party library types), use ",(0,i.jsx)(n.code,{children:"interface"})," and disable the lint rule where necessary."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// types.ts\ndeclare namespace NodeJS {\n // eslint-disable-next-line @typescript-eslint/consistent-type-definitions\n export interface ProcessEnv {\n NODE_ENV: 'development' | 'production';\n PORT: string;\n CUSTOM_ENV_VAR: string;\n }\n}\n\n// server.ts\napp.listen(process.env.PORT, () => {...}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"array-types",children:"Array Types"}),"\n",(0,i.jsx)(o.jO,{prefix:"Array types should be defined using generic syntax",href:"https://typescript-eslint.io/rules/array-type/#generic",children:"'@typescript-eslint/array-type': ['error', { default: 'generic' }]"}),"\n",(0,i.jsx)(o.L7,{children:(0,i.jsx)(n.p,{children:"Since there is no functional difference between the 'generic' and 'array' definitions, feel free to choose the one\nthat your team finds most readable."})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst x: string[] = ['foo', 'bar'];\nconst y: readonly string[] = ['foo', 'bar'];\n\n// \u2705 Use\nconst x: Array = ['foo', 'bar'];\nconst y: ReadonlyArray = ['foo', 'bar'];\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type-imports-and-exports",children:"Type Imports and Exports"}),"\n",(0,i.jsxs)(n.p,{children:["TypeScript allows specifying a ",(0,i.jsx)(n.code,{children:"type"})," keyword on imports to indicate that the export exists only in the type system, not at runtime."]}),"\n",(0,i.jsx)(n.p,{children:"Type imports must always be separated:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Tree Shaking and Dead Code Elimination: If you use ",(0,i.jsx)(n.code,{children:"import"})," for types instead of ",(0,i.jsx)(n.code,{children:"import type"}),", the bundler might include the imported module in the bundle unnecessarily, increasing the size. Separating imports ensures that only necessary runtime code is included."]}),"\n",(0,i.jsx)(n.li,{children:"Minimizing Dependencies: Some modules may contain both runtime and type definitions. Mixing type imports with runtime imports might lead to accidental inclusion of unnecessary runtime code."}),"\n",(0,i.jsx)(n.li,{children:"Improves code clarity by making the distinction between runtime dependencies and type-only imports explicit."}),"\n"]}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/consistent-type-imports/",children:"'@typescript-eslint/consistent-type-imports': 'error'"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid using `import` for both runtime and type\nimport { MyClass } from 'some-library';\n\n// Even if MyClass is only a type, the entire module might be included in the bundle.\n\n// \u2705 Use `import type`\nimport type { MyClass } from 'some-library';\n\n// This ensures only the type is imported and no runtime code from \"some-library\" ends up in the bundle.\n"})}),"\n",(0,i.jsx)(n.h3,{id:"services",children:"Services"}),"\n",(0,i.jsx)(n.p,{children:"Documentation becomes outdated the moment it's written, and worse than no documentation is wrong documentation. The same applies to types when describing the modules your app interacts with, such as APIs, messaging systems, databases etc."}),"\n",(0,i.jsxs)(n.p,{children:["For external API services, such as REST, GraphQL etc. it's crucial to generate types from their contracts, whether they use Swagger, schemas, or other sources (e.g. ",(0,i.jsx)(n.a,{href:"https://github.com/drwpow/openapi-typescript",children:"openapi-ts"}),", ",(0,i.jsx)(n.a,{href:"https://github.com/kamilkisiela/graphql-config",children:"graphql-config"})," etc.). Avoid manually declaring and maintaining types, as they can easily fall out of sync."]}),"\n",(0,i.jsx)(n.p,{children:"As an exception manually declare types only when there is truly no documentation provided by external service."}),"\n",(0,i.jsx)(n.h2,{id:"functions",children:"Functions"}),"\n",(0,i.jsx)(n.p,{children:"Function conventions should be followed as much as possible (some of the conventions derive from functional programming basic concepts):"}),"\n",(0,i.jsx)(n.h3,{id:"general",children:"General"}),"\n",(0,i.jsx)(n.p,{children:"Function:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"should have single responsibility."}),"\n",(0,i.jsx)(n.li,{children:"should be stateless where the same input arguments return same value every single time."}),"\n",(0,i.jsx)(n.li,{children:"should accept at least one argument and return data."}),"\n",(0,i.jsx)(n.li,{children:"should not have side effects, but be pure. Implementation should not modify or access variable value outside its local environment (global state, fetching etc.)."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"single-object-arg",children:"Single Object Arg"}),"\n",(0,i.jsxs)(n.p,{children:["To keep function readable and easily extensible for the future (adding/removing args), strive to have single object as the function arg, instead of multiple args.",(0,i.jsx)(n.br,{}),"\n","As an exception this does not apply when having only one primitive single arg (e.g. simple functions isNumber(value), implementing currying etc.)."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid having multiple arguments\ntransformUserInput('client', false, 60, 120, null, true, 2000);\n\n// \u2705 Use options object as argument\ntransformUserInput({\n method: 'client',\n isValidated: false,\n minLines: 60,\n maxLines: 120,\n defaultInput: null,\n shouldLog: true,\n timeout: 2000,\n});\n"})}),"\n",(0,i.jsx)(n.h3,{id:"required--optional-args",children:"Required & Optional Args"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Strive to have majority of args required and use optional sparingly."}),(0,i.jsx)(n.br,{}),"\n","If the function becomes too complex, it probably should be broken into smaller pieces.",(0,i.jsx)(n.br,{}),"\n",'An exaggerated example where implementing 10 functions with 5 required args each, is better then implementing one "can do it all" function that accepts 50 optional args.']}),"\n",(0,i.jsx)(n.h3,{id:"args-as-discriminated-type",children:"Args as Discriminated Type"}),"\n",(0,i.jsxs)(n.p,{children:["When applicable use ",(0,i.jsx)(n.strong,{children:"discriminated union type"})," to eliminate optional properties, which will decrease complexity on function API and only required properties will be passed depending on its use case."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid optional properties as they increase complexity of function API\ntype StatusParams = {\n data?: Products;\n title?: string;\n time?: number;\n error?: string;\n};\n\n// \u2705 Strive to have majority of properties required, if that's not possible,\n// use discriminated union for clear intent on function usage\ntype StatusSuccessParams = {\n status: 'success';\n data: Products;\n title: string;\n};\n\ntype StatusLoadingParams = {\n status: 'loading';\n time: number;\n};\n\ntype StatusErrorParams = {\n status: 'error';\n error: string;\n};\n\n// Discriminated function param 'StatusParams' with no optional properties\ntype StatusParams = StatusSuccessParams | StatusLoadingParams | StatusErrorParams;\n\nexport const parseStatus = (params: StatusParams) => {...\n"})}),"\n",(0,i.jsx)(n.h2,{id:"variables",children:"Variables"}),"\n",(0,i.jsx)(n.h3,{id:"const-assertion",children:"Const Assertion"}),"\n",(0,i.jsxs)(n.p,{children:["Strive to use const assertion ",(0,i.jsx)(n.code,{children:"as const"}),":"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"type is narrowed"}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["object gets ",(0,i.jsx)(n.code,{children:"readonly"})," properties"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["array becomes ",(0,i.jsx)(n.code,{children:"readonly"})," tuple"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid declaring constants without const assertion\nconst FOO_LOCATION = { x: 50, y: 130 }; // Type { x: number; y: number; }\nFOO_LOCATION.x = 10;\n\nconst BAR_LOCATION = [50, 130]; // Type number[]\nBAR_LOCATION.push(10);\n\nconst RATE_LIMIT = 25;\nconst RATE_LIMIT_MESSAGE = `Max number of requests/min is ${RATE_LIMIT}.`; // Type string\n\n// \u2705 Use const assertion\nconst FOO_LOCATION = { x: 50, y: 130 } as const; // Type '{ readonly x: 50; readonly y: 130; }'\nFOO_LOCATION.x = 10; // Error\n\nconst BAR_LOCATION = [50, 130] as const; // Type 'readonly [10, 20]'\nBAR_LOCATION.push(10); // Error\n\nconst RATE_LIMIT = 25;\nconst RATE_LIMIT_MESSAGE = `Max number of requests/min is ${RATE_LIMIT}.` as const; // Type 'Rate limit exceeded! Max number of requests/min is 25.'\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"enums--const-assertion",children:"Enums & Const Assertion"}),"\n",(0,i.jsx)(n.p,{children:"Const assertion must be used over enum."}),"\n",(0,i.jsxs)(n.p,{children:["While enums can still cover use cases as const assertion would, we tend to avoid it. Some of the reasonings as mentioned in TypeScript documentation - ",(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls",children:"Const enum pitfalls"}),", ",(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums",children:"Objects vs Enums"}),", ",(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings",children:"Reverse mappings"})," etc."]}),"\n",(0,i.jsx)(o.jO,{href:"https://eslint.org/docs/latest/rules/no-restricted-syntax",children:"'no-restricted-syntax': [\n 'error',\n {\n selector: 'TSEnumDeclaration',\n message: 'Use const assertion or a string union type instead.',\n },\n]"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid using enums\nenum UserRole {\n GUEST,\n MODERATOR,\n ADMINISTRATOR,\n}\n\nenum Color {\n PRIMARY = '#B33930',\n SECONDARY = '#113A5C',\n BRAND = '#9C0E7D',\n}\n\n// \u2705 Use const assertion\nconst USER_ROLES = ['guest', 'moderator', 'administrator'] as const;\ntype UserRole = (typeof USER_ROLES)[number]; // Type \"guest\" | \"moderator\" | \"administrator\"\n\n// Use satisfies if UserRole type is already defined - e.g. database schema model\ntype UserRoleDB = ReadonlyArray<'guest' | 'moderator' | 'administrator'>;\nconst AVAILABLE_ROLES = ['guest', 'moderator'] as const satisfies UserRoleDB;\n\nconst COLOR = {\n primary: '#B33930',\n secondary: '#113A5C',\n brand: '#9C0E7D',\n} as const;\ntype Color = typeof COLOR;\ntype ColorKey = keyof Color; // Type \"primary\" | \"secondary\" | \"brand\"\ntype ColorValue = Color[ColorKey]; // Type \"#B33930\" | \"#113A5C\" | \"#9C0E7D\"\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type-union--boolean-flags",children:"Type Union & Boolean Flags"}),"\n",(0,i.jsx)(n.p,{children:"Embrace type unions, especially when type union options are mutually exclusive, instead multiple boolean flag variables."}),"\n",(0,i.jsx)(n.p,{children:"Boolean flags have a tendency to accumulate over time, leading to confusing and error-prone code, since they hide the actual app state."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid introducing multiple boolean flag variables\nconst isPending, isProcessing, isConfirmed, isExpired;\n\n// \u2705 Use type union variable\ntype UserStatus = 'pending' | 'processing' | 'confirmed' | 'expired';\nconst userStatus: UserStatus;\n"})}),"\n",(0,i.jsxs)(n.p,{children:["When boolean flags are used and the number of possible states grows quickly, it often results in unhandled or ambiguous states. Instead, take advantage of ",(0,i.jsx)(n.a,{href:"#discriminated-union",children:"discriminated unions"})," to better manage and represent your application's state."]}),"\n",(0,i.jsx)(n.h3,{id:"null--undefined",children:"Null & Undefined"}),"\n",(0,i.jsxs)(n.p,{children:["In TypeScript types ",(0,i.jsx)(n.code,{children:"null"})," and ",(0,i.jsx)(n.code,{children:"undefined"})," many times can be used interchangeably.",(0,i.jsx)(n.br,{}),"\n","Strive to:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Use ",(0,i.jsx)(n.code,{children:"null"})," to explicitly state it has no value - assignment, return function type etc."]}),"\n",(0,i.jsxs)(n.li,{children:["Use ",(0,i.jsx)(n.code,{children:"undefined"})," assignment when the value doesn't exist. E.g. exclude fields in form, request payload, database query (",(0,i.jsx)(n.a,{href:"https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined",children:"Prisma differentiation"}),") etc."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"naming",children:"Naming"}),"\n",(0,i.jsx)(n.p,{children:"Strive to keep naming conventions consistent and readable, with important context provided, because another person will maintain the code you have written."}),"\n",(0,i.jsx)(n.h3,{id:"named-export",children:"Named Export"}),"\n",(0,i.jsx)(o.jO,{prefix:"Named exports must be used to ensure that all imports follow a uniform pattern",href:"https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-default-export.md",children:'\'import/no-default-export\': \'error\'\n\n// In case of exceptions disable the rule\noverrides: [\n{\nfiles: ["src/pages/**/*"],\nrules: { "import/no-default-export": "off" },\n}\n]\n'}),"\n",(0,i.jsx)(n.p,{children:"This keeps variables, functions etc. names consistent across the entire codebase. Named exports have the benefit of\nerroring when import statements try to import something that hasn't been declared."}),"\n",(0,i.jsx)(n.h3,{id:"naming-conventions",children:"Naming Conventions"}),"\n",(0,i.jsx)(n.p,{children:"While it's often hard to find the best name, aim to optimize code for consistency and future reader by following conventions:"}),"\n",(0,i.jsx)(n.h4,{id:"variables-1",children:"Variables"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Locals"}),(0,i.jsx)(n.br,{}),"\n","Camel case",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"products"}),", ",(0,i.jsx)(n.code,{children:"productsFiltered"})]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Booleans"}),(0,i.jsx)(n.br,{}),"\n","Prefixed with ",(0,i.jsx)(n.code,{children:"is"}),", ",(0,i.jsx)(n.code,{children:"has"})," etc.",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"isDisabled"}),", ",(0,i.jsx)(n.code,{children:"hasProduct"})]}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/naming-convention",children:"'@typescript-eslint/naming-convention': [\n 'error',\n {\n selector: 'variable',\n types: ['boolean'],\n format: ['PascalCase'],\n prefix: ['is', 'are', 'should', 'has', 'can', 'did', 'will'],\n }\n]\n"}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Constants"}),(0,i.jsx)(n.br,{}),"\n","Capitalized",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"PRODUCT_ID"})]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Object constants"})}),"\n",(0,i.jsx)(n.p,{children:"Singular, capitalized with const assertion."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"const ORDER_STATUS = {\n pending: 'pending',\n fulfilled: true,\n error: 'Shipping Error',\n} as const;\n"})}),"\n",(0,i.jsxs)(n.p,{children:["If type exist use ",(0,i.jsx)(n.code,{children:"satisfies"})," operator in conjunction with const assertion, to conform object matches its type."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// OrderStatus is predefined (e.g. generated from database schema, API)\ntype OrderStatus = {\n pending: 'pending' | 'idle';\n fulfilled: boolean;\n error: string;\n};\n\nconst PENDING_STATUS = {\n pending: 'pending',\n fulfilled: true,\n error: 'Shipping Error',\n} as const satisfies OrderStatus;\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h4,{id:"functions-1",children:"Functions"}),"\n",(0,i.jsxs)(n.p,{children:["Camel case",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"filterProductsByType"}),", ",(0,i.jsx)(n.code,{children:"formatCurrency"})]}),"\n",(0,i.jsx)(n.h4,{id:"types-1",children:"Types"}),"\n",(0,i.jsxs)(n.p,{children:["Pascal case",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"OrderStatus"}),", ",(0,i.jsx)(n.code,{children:"ProductItem"})]}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/naming-convention",children:"\n'@typescript-eslint/naming-convention': [\n'error',\n{\n selector: 'typeAlias',\n format: ['PascalCase'],\n},\n]"}),"\n",(0,i.jsx)(n.h4,{id:"generics",children:"Generics"}),"\n",(0,i.jsxs)(n.p,{children:["A generic variable must start with the capital letter T followed by a descriptive name ",(0,i.jsx)(n.code,{children:"TRequest"}),", ",(0,i.jsx)(n.code,{children:"TFooBar"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Creating more complex types often include generics, which can make them hard to read and understand, that's why we try to put best effort when naming them.",(0,i.jsx)(n.br,{}),"\n","Naming generics using popular convention with one letter ",(0,i.jsx)(n.code,{children:"T"}),", ",(0,i.jsx)(n.code,{children:"K"})," etc. is not allowed, the more variables we introduce, the easier it is to mistake them."]}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/naming-convention",children:"'@typescript-eslint/naming-convention': [\n 'error',\n {\n // Generic type parameter must start with letter T, followed by any uppercase letter.\n selector: 'typeParameter',\n format: ['PascalCase'],\n custom: { regex: '^T[A-Z]', match: true },\n }\n]"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid naming generics with one letter\nconst createPair = (first: T, second: K): [T, K] => {\n return [first, second];\n};\nconst pair = createPair(1, 'a');\n\n// \u2705 Name starts with the capital letter T followed by a descriptive name\nconst createPair = (first: TFirst, second: TSecond): [TFirst, TSecond] => {\n return [first, second];\n};\nconst pair = createPair(1, 'a');\n"})}),"\n",(0,i.jsx)(n.h4,{id:"abbreviations--acronyms",children:"Abbreviations & Acronyms"}),"\n",(0,i.jsx)(n.p,{children:"Treat acronyms as whole words, with capitalized first letter only."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst FAQList = ['qa-1', 'qa-2'];\nconst generateUserURL(params) => {...}\n\n// \u2705 Use\nconst FaqList = ['qa-1', 'qa-2'];\nconst generateUserUrl(params) => {...}\n"})}),"\n",(0,i.jsx)(n.p,{children:"In favor of readability, strive to avoid abbreviations, unless they are widely accepted and necessary."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst GetWin(params) => {...}\n\n// \u2705 Use\nconst GetWindow(params) => {...}\n"})}),"\n",(0,i.jsx)(n.h4,{id:"react-components",children:"React Components"}),"\n",(0,i.jsxs)(n.p,{children:["Pascal case",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"ProductItem"}),", ",(0,i.jsx)(n.code,{children:"ProductsPage"})]}),"\n",(0,i.jsx)(n.h4,{id:"prop-types",children:"Prop Types"}),"\n",(0,i.jsxs)(n.p,{children:['React component name following "Props" postfix',(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"[ComponentName]Props"})," - ",(0,i.jsx)(n.code,{children:"ProductItemProps"}),", ",(0,i.jsx)(n.code,{children:"ProductsPageProps"})]}),"\n",(0,i.jsx)(n.h4,{id:"callback-props",children:"Callback Props"}),"\n",(0,i.jsxs)(n.p,{children:["Event handler (callback) props are prefixed as ",(0,i.jsx)(n.code,{children:"on*"})," - e.g. ",(0,i.jsx)(n.code,{children:"onClick"}),".",(0,i.jsx)(n.br,{}),"\n","Event handler implementation functions are prefixed as ",(0,i.jsx)(n.code,{children:"handle*"})," - e.g. ",(0,i.jsx)(n.code,{children:"handleClick"}),"."]}),"\n",(0,i.jsx)(o.jO,{href:"https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-handler-names.md",children:"'react/jsx-handler-names': [\n 'error',\n {\n eventHandlerPrefix: 'handle',\n eventHandlerPropPrefix: 'on',\n },\n]"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-tsx",children:"// \u274c Avoid inconsistent callback prop naming\n\n\n\n// \u2705 Use prop prefix 'on*' and handler prefix 'handle*'\n\n\n"})}),"\n",(0,i.jsx)(n.h4,{id:"react-hooks",children:"React Hooks"}),"\n",(0,i.jsx)(o.jO,{prefix:"Camel case, prefixed as 'use'",href:"https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks",children:"'react-hooks/rules-of-hooks': 'error'"}),"\n",(0,i.jsx)(o.jO,{prefix:"Symmetrically convention as [value, setValue] = useState()",href:"https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/hook-use-state.md#rule-details",children:"'react/hook-use-state': 'error'"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid inconsistent useState hook naming\nconst [userName, setUser] = useState();\nconst [color, updateColor] = useState();\nconst [isActive, setActive] = useState();\n\n// \u2705 Use\nconst [name, setName] = useState();\nconst [color, setColor] = useState();\nconst [isActive, setIsActive] = useState();\n"})}),"\n",(0,i.jsx)(n.p,{children:"Custom hook must always return an object"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst [products, errors] = useGetProducts();\nconst [fontSizes] = useTheme();\n\n// \u2705 Use\nconst { products, errors } = useGetProducts();\nconst { fontSizes } = useTheme();\n"})}),"\n",(0,i.jsx)(n.h3,{id:"comments",children:"Comments"}),"\n",(0,i.jsx)(n.p,{children:"In general try to avoid comments, by writing expressive code and name things what they are."}),"\n",(0,i.jsxs)(n.p,{children:["Use comments when you need to add context or explain choices that cannot be expressed through code (e.g. config files).",(0,i.jsx)(n.br,{}),"\n","Comments should always be complete sentences. As rule of thumb try to explain ",(0,i.jsx)(n.code,{children:"why"})," in comments, not ",(0,i.jsx)(n.code,{children:"how"})," and ",(0,i.jsx)(n.code,{children:"what"}),"."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\n// convert to minutes\nconst m = s * 60;\n// avg users per minute\nconst myAvg = u / m;\n\n// \u2705 Use - Expressive code and name things what they are\nconst SECONDS_IN_MINUTE = 60;\nconst minutes = seconds * SECONDS_IN_MINUTE;\nconst averageUsersPerMinute = noOfUsers / minutes;\n\n// \u2705 Use - Add context to explain why in comments\n// TODO: Filtering should be moved to the backend once API changes are released.\n// Issue/PR - https://github.com/foo/repo/pulls/55124\nconst filteredUsers = frontendFiltering(selectedUsers);\n// Use Fourier transformation to minimize information loss - https://github.com/dntj/jsfft#usage\nconst frequencies = signal.FFT();\n"})}),"\n",(0,i.jsx)(n.h2,{id:"source-organization",children:"Source Organization"}),"\n",(0,i.jsx)(n.h3,{id:"code-collocation",children:"Code Collocation"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Every application or package in monorepo has project files/folders organized and grouped ",(0,i.jsx)(n.strong,{children:"by feature"}),"."]}),"\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.strong,{children:"Collocate code as close as possible to where it's relevant."})}),"\n",(0,i.jsx)(n.li,{children:"Deep folder nesting should not represent an issue."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"imports",children:"Imports"}),"\n",(0,i.jsxs)(n.p,{children:["Import paths can be relative, starting with ",(0,i.jsx)(n.code,{children:"./"})," or ",(0,i.jsx)(n.code,{children:"../"}),", or they can be absolute ",(0,i.jsx)(n.code,{children:"@common/utils"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"To make import statements more readable and easier to understand:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Relative"})," imports ",(0,i.jsx)(n.code,{children:"./sortItems"})," must be used when importing files within the same feature, that are 'close' to each other, which also allows moving feature around the codebase without introducing changes in these imports."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Absolute"})," imports ",(0,i.jsx)(n.code,{children:"@common/utils"})," must be used in all other cases."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"All"})," imports must be auto sorted by tooling e.g. ",(0,i.jsx)(n.a,{href:"https://github.com/trivago/prettier-plugin-sort-imports",children:"prettier-plugin-sort-imports"}),", ",(0,i.jsx)(n.a,{href:"https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md",children:"eslint-plugin-import"})," etc."]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nimport { bar, foo } from '../../../../../../distant-folder';\n\n// \u2705 Use\nimport { locationApi } from '@api/locationApi';\n\nimport { foo } from '../../foo';\nimport { bar } from '../bar';\nimport { baz } from './baz';\n"})}),"\n",(0,i.jsx)(n.h3,{id:"project-structure",children:"Project Structure"}),"\n",(0,i.jsx)(n.p,{children:"Example frontend monorepo project, where every application has file/folder grouped by feature:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"apps/\n\u251c\u2500 product-manager/\n\u2502 \u251c\u2500 common/\n\u2502 \u2502 \u251c\u2500 components/\n\u2502 \u2502 \u2502 \u251c\u2500 Button/\n\u2502 \u2502 \u2502 \u251c\u2500 ProductTitle/\n\u2502 \u2502 \u2502 \u251c\u2500 ...\n\u2502 \u2502 \u2502 \u2514\u2500 index.tsx\n\u2502 \u2502 \u251c\u2500 consts/\n\u2502 \u2502 \u2502 \u251c\u2500 paths.ts\n\u2502 \u2502 \u2502 \u2514\u2500 ...\n\u2502 \u2502 \u251c\u2500 hooks/\n\u2502 \u2502 \u2514\u2500 types/\n\u2502 \u251c\u2500 modules/\n\u2502 \u2502 \u251c\u2500 HomePage/\n\u2502 \u2502 \u251c\u2500 ProductAddPage/\n\u2502 \u2502 \u251c\u2500 ProductPage/\n\u2502 \u2502 \u251c\u2500 ProductsPage/\n\u2502 \u2502 \u2502 \u251c\u2500 api/\n\u2502 \u2502 \u2502 \u2502 \u2514\u2500 useGetProducts/\n\u2502 \u2502 \u2502 \u251c\u2500 components/\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500 ProductItem/\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500 ProductsStatistics/\n\u2502 \u2502 \u2502 \u2502 \u2514\u2500 ...\n\u2502 \u2502 \u2502 \u251c\u2500 utils/\n\u2502 \u2502 \u2502 \u2502 \u2514\u2500 filterProductsByType/\n\u2502 \u2502 \u2502 \u2514\u2500 index.tsx\n\u2502 \u2502 \u251c\u2500 ...\n\u2502 \u2502 \u2514\u2500 index.tsx\n\u2502 \u251c\u2500 eslintrc.js\n\u2502 \u251c\u2500 package.json\n\u2502 \u2514\u2500 tsconfig.json\n\u251c\u2500 warehouse/\n\u251c\u2500 admin-dashboard/\n\u2514\u2500 ...\n"})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"modules"})," folder is responsible for implementation of each individual page, where all custom features for that page are being implemented (components, hooks, utils functions etc.)."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"common"}),' folder is responsible for implementations that are truly used across application. Since it\'s a "global folder" it should be used sparingly.',(0,i.jsx)(n.br,{}),"\n","If same component e.g. ",(0,i.jsx)(n.code,{children:"common/components/ProductTitle"})," starts being used on more than one page, it shall be moved to common folder."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["In case using frontend framework with file-system based router (e.g. Nextjs), ",(0,i.jsx)(n.code,{children:"pages"})," folder serves only as a router, where its responsibility is to define routes (no business logic implementation)."]}),"\n",(0,i.jsx)(n.p,{children:"Example backend project structure with file/folder grouped by feature:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"product-manager/\n\u251c\u2500 dist/\n\u251c\u2500\u2500 database/\n\u2502 \u251c\u2500\u2500 migrations/\n\u2502 \u2502 \u251c\u2500\u2500 20220102063048_create_accounts.ts\n\u2502 \u2502 \u2514\u2500\u2500 ...\n\u2502 \u2514\u2500\u2500 seeders/\n\u2502 \u251c\u2500\u2500 20221116042655-feeds.ts\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500 docker/\n\u251c\u2500 logs/\n\u251c\u2500 scripts/\n\u251c\u2500 src/\n\u2502 \u251c\u2500 common/\n\u2502 \u2502 \u251c\u2500 consts/\n\u2502 \u2502 \u251c\u2500 middleware/\n\u2502 \u2502 \u251c\u2500 types/\n\u2502 \u2502 \u2514\u2500 ...\n\u2502 \u251c\u2500 modules/\n\u2502 \u2502 \u251c\u2500\u2500 admin/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 account/\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.model.ts\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.controller.ts\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.route.ts\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.service.ts\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.validation.ts\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.test.ts\n\u2502 \u2502 \u2502 \u2502 \u2514\u2500\u2500 index.ts\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 ...\n\u2502 \u2502 \u251c\u2500\u2500 general/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.model.ts\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.controller.ts\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.route.ts\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.service.ts\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.validation.ts\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.test.ts\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 index.ts\n\u2502 \u2502 \u251c\u2500 ...\n\u2502 \u2502 \u2514\u2500 index.tsx\n\u2502 \u2514\u2500 ...\n\u251c\u2500 ...\n\u251c\u2500 eslintrc.js\n\u251c\u2500 package.json\n\u2514\u2500 tsconfig.json\n"})}),"\n",(0,i.jsx)(n.h2,{id:"appendix---react",children:"Appendix - React"}),"\n",(0,i.jsxs)(n.p,{children:["Since React components and hooks are also functions, respective ",(0,i.jsx)(n.a,{href:"#functions",children:"function conventions"})," applies."]}),"\n",(0,i.jsx)(n.h3,{id:"required--optional-props",children:"Required & Optional Props"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Strive to have majority of props required and use optional sparingly."})}),"\n",(0,i.jsxs)(n.p,{children:["Especially when creating new component for first/single use case majority of props should be required. When component starts covering more use cases, introduce optional props.",(0,i.jsx)(n.br,{}),"\n","There are potential exceptions, where component API needs to implement optional props from the start (e.g. shared components covering multiple use cases, UI design system components - button ",(0,i.jsx)(n.code,{children:"isDisabled"})," etc.)"]}),"\n",(0,i.jsxs)(n.p,{children:["If component/hook becomes to complex it probably should be broken into smaller pieces.",(0,i.jsx)(n.br,{}),"\n",'An exaggerated example where implementing 10 React components with 5 required props each, is better then implementing one "can do it all" component that accepts 50 optional props.']}),"\n",(0,i.jsx)(n.h3,{id:"props-as-discriminated-type",children:"Props as Discriminated Type"}),"\n",(0,i.jsxs)(n.p,{children:["When applicable use ",(0,i.jsx)(n.strong,{children:"discriminated type"})," to eliminate optional props, which will decrease complexity on component API and only required props will be passed depending on its use case."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid optional props as they increase complexity of component API\ntype StatusProps = {\n data?: Products;\n title?: string;\n time?: number;\n error?: string;\n};\n\n// \u2705 Strive to have majority of props required, if that's not possible,\n// use discriminated union for clear intent on component usage\ntype StatusSuccess = {\n status: 'success';\n data: Products;\n title: string;\n};\n\ntype StatusLoading = {\n status: 'loading';\n time: number;\n};\n\ntype StatusError = {\n status: 'error';\n error: string;\n};\n\n// Discriminated component props 'StatusProps' with no optional properties\ntype StatusProps = StatusSuccess | StatusLoading | StatusError;\n\nexport const Status = (status: StatusProps) => {...\n"})}),"\n",(0,i.jsx)(n.h3,{id:"props-to-state",children:"Props To State"}),"\n",(0,i.jsxs)(n.p,{children:["In general avoid using props to state, since component will not update on prop changes. It can lead to bugs that are hard to track, with unintended side effects and difficulty testing.",(0,i.jsx)(n.br,{}),"\n","When there is truly a use case for using prop in initial state, prop must be prefixed with ",(0,i.jsx)(n.code,{children:"initial"})," (e.g. ",(0,i.jsx)(n.code,{children:"initialProduct"}),", ",(0,i.jsx)(n.code,{children:"initialSort"})," etc.)"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-tsx",children:"// \u274c Avoid using props to state\ntype FooProps = {\n productName: string;\n userId: string;\n};\n\nexport const Foo = ({ productName, userId }: FooProps) => {\n const [productName, setProductName] = useState(productName);\n ...\n\n// \u2705 Use prop prefix `initial`, when there is a rational use case for it\ntype FooProps = {\n initialProductName: string;\n userId: string;\n};\n\nexport const Foo = ({ initialProductName, userId }: FooProps) => {\n const [productName, setProductName] = useState(initialProductName);\n ...\n"})}),"\n",(0,i.jsx)(n.h3,{id:"props-type",children:"Props Type"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-tsx",children:"// \u274c Avoid using React.FC type\ntype FooProps = {\n name: string;\n score: number;\n};\n\nexport const Foo: React.FC = ({ name, score }) => {\n\n// \u2705 Use props argument with type\ntype FooProps = {\n name: string;\n score: number;\n};\n\nexport const Foo = ({ name, score }: FooProps) => {...\n"})}),"\n",(0,i.jsx)(n.h3,{id:"component-types",children:"Component Types"}),"\n",(0,i.jsx)(n.h4,{id:"container",children:"Container"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:['All container components have postfix "Container" or "Page" ',(0,i.jsx)(n.code,{children:"[ComponentName]Container|Page"}),'. Use "Page" postfix to indicate component is an actual web page.']}),"\n",(0,i.jsxs)(n.li,{children:["Each feature has a container component (",(0,i.jsx)(n.code,{children:"AddUserContainer.tsx"}),", ",(0,i.jsx)(n.code,{children:"EditProductContainer.tsx"}),", ",(0,i.jsx)(n.code,{children:"ProductsPage.tsx"})," etc.)"]}),"\n",(0,i.jsx)(n.li,{children:"Includes business logic."}),"\n",(0,i.jsx)(n.li,{children:"API integration."}),"\n",(0,i.jsxs)(n.li,{children:["Structure:","\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"ProductsPage/\n\u251c\u2500 api/\n\u2502 \u2514\u2500 useGetProducts/\n\u251c\u2500 components/\n\u2502 \u2514\u2500 ProductItem/\n\u251c\u2500 utils/\n\u2502 \u2514\u2500 filterProductsByType/\n\u2514\u2500 index.tsx\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h4,{id:"ui---feature",children:"UI - Feature"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Representational components that are designed to fulfill feature requirements."}),"\n",(0,i.jsx)(n.li,{children:"Nested inside container component folder."}),"\n",(0,i.jsxs)(n.li,{children:["Should follow ",(0,i.jsx)(n.a,{href:"#functions",children:"functions conventions"})," as much as possible."]}),"\n",(0,i.jsx)(n.li,{children:"No API integration."}),"\n",(0,i.jsxs)(n.li,{children:["Structure:","\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"ProductItem/\n\u251c\u2500 index.tsx\n\u251c\u2500 ProductItem.stories.tsx\n\u2514\u2500 ProductItem.test.tsx\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h4,{id:"ui---design-system",children:"UI - Design system"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Global Reusable/shared components used throughout whole codebase."}),"\n",(0,i.jsxs)(n.li,{children:["Structure:","\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"Button/\n\u251c\u2500 index.tsx\n\u251c\u2500 Button.stories.tsx\n\u2514\u2500 Button.test.tsx\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"store--pass-data",children:"Store & Pass Data"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Pass only the necessary props to child components rather than passing the entire object."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Utilize storing state in the URL, especially for filtering, sorting etc."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Don't sync URL state with local state."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Consider passing data simply through props, using the URL, or composing children. Use global state (Zustand, Context) as a last resort."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["Use React compound components when components should belong and work together: ",(0,i.jsx)(n.code,{children:"menu"}),", ",(0,i.jsx)(n.code,{children:"accordion"}),",",(0,i.jsx)(n.code,{children:"navigation"}),", ",(0,i.jsx)(n.code,{children:"tabs"}),", ",(0,i.jsx)(n.code,{children:"list"}),", etc.",(0,i.jsx)(n.br,{}),"\n","Always export compound components as:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-tsx",children:'// PriceList.tsx\nconst PriceListRoot = ({ children }) =>
{children}
;\nconst PriceListItem = ({ title, amount }) =>
Name: {name} - Amount: {amount}
;\n\n// \u274c\nexport const PriceList = {\n Container: PriceListRoot,\n Item: PriceListItem,\n};\n// \u274c\nPriceList.Item = Item;\nexport default PriceList;\n\n// \u2705\nexport const PriceList = PriceListRoot as typeof PriceListRoot & {\n Item: typeof PriceListItem;\n};\nPriceList.Item = PriceListItem;\n\n// App.tsx\nimport { PriceList } from "./PriceList";\n\n\n \n \n;\n'})}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"UI components should show derived state and send events, nothing more (no business logic)."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["As in many programming languages functions args can be passed to the next function and on to the next etc.",(0,i.jsx)(n.br,{}),"\n","React components are no different, where prop drilling should not become an issue.",(0,i.jsx)(n.br,{}),"\n","If with app scaling prop drilling truly becomes an issue, try to refactor render method, local states in parent components, using composition etc."]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Data fetching is only allowed in container components."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["Use of server-state library is encouraged (",(0,i.jsx)(n.a,{href:"https://github.com/tanstack/query",children:"react-query"}),", ",(0,i.jsx)(n.a,{href:"https://github.com/apollographql/apollo-client",children:"apollo client"})," etc.)."]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["Use of client-state library for global state is discouraged.",(0,i.jsx)(n.br,{}),"\n","Reconsider if something should be truly global across application, e.g. ",(0,i.jsx)(n.code,{children:"themeMode"}),", ",(0,i.jsx)(n.code,{children:"Permissions"})," or even that can be put in server-state (e.g. user settings - ",(0,i.jsx)(n.code,{children:"/me"})," endpoint). If still global state is truly needed use ",(0,i.jsx)(n.a,{href:"https://github.com/pmndrs/zustand",children:"Zustand"})," or ",(0,i.jsx)(n.a,{href:"https://react.dev/reference/react/createContext",children:"Context"}),"."]}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"appendix---tests",children:"Appendix - Tests"}),"\n",(0,i.jsx)(n.h3,{id:"what--how-to-test",children:"What & How To Test"}),"\n",(0,i.jsxs)(n.p,{children:["Automated test comes with benefits that helps us write better code and makes it easy to refactor, while bugs are caught earlier in the process.",(0,i.jsx)(n.br,{}),"\n","Consider trade-offs of what and how to test to achieve confidence application is working as intended, while writing and maintaining tests doesn't slow the team down."]}),"\n",(0,i.jsx)(n.p,{children:"\u2705 Do:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Implement test to be short, explicit, and pleasant to work with. Intent of a test should be immediately visible."}),"\n",(0,i.jsxs)(n.li,{children:["Strive for AAA pattern, to maintain clean, organized, and understandable unit tests.","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Arrange - Setup preconditions or the initial state necessary for the test case. Create necessary objects and define input values."}),"\n",(0,i.jsxs)(n.li,{children:["Act - Perform the action you want to unit test (invoke a method, triggering an event etc.). ",(0,i.jsx)(n.strong,{children:"Strive for minimal number of actions"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["Assert - Validate the outcome against expectations. ",(0,i.jsx)(n.strong,{children:"Strive for minimal number of asserts"}),".",(0,i.jsx)(n.br,{}),"\n",'A rule "unit tests should fail for exactly one reason" doesn\'t need to apply always, but it can indicate a code smell if there are tests with many asserts in a codebase.']}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["As mentioned in ",(0,i.jsx)(n.a,{href:"#functions",children:"function conventions"})," try to keep them pure, and impure one small and focused.",(0,i.jsx)(n.br,{}),"\n","It makes them easy to test, by passing args and observing return values, since we will ",(0,i.jsx)(n.strong,{children:"rarely need to mock dependencies"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["Strive to write tests in a way your app is used by a user, meaning test business logic.",(0,i.jsx)(n.br,{}),"\n","E.g. For a specific user role or permission, given some input, we receive the expected output from the process."]}),"\n",(0,i.jsx)(n.li,{children:"Make tests as isolated as possible, where they don't depend on order of execution and should run independently with its own local storage, session storage, data, cookies etc.\nTest isolation speeds up the test run, improves reproducibility, makes debugging easier and prevents cascading test failures."}),"\n",(0,i.jsxs)(n.li,{children:["Tests should be resilient to changes.","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Black box testing - Always test only implementation that is publicly exposed, don't write fragile tests on how implementation works internally."}),"\n",(0,i.jsxs)(n.li,{children:["Query HTML elements based on attributes that are unlikely to change. Order of priority must be followed as specified in ",(0,i.jsx)(n.a,{href:"https://testing-library.com/docs/queries/about/#priority",children:"Testing Library"})," - ",(0,i.jsx)(n.a,{href:"https://testing-library.com/docs/queries/byrole",children:"role"}),", ",(0,i.jsx)(n.a,{href:"https://testing-library.com/docs/queries/bylabeltext",children:"label"}),", ",(0,i.jsx)(n.a,{href:"https://testing-library.com/docs/queries/byplaceholdertext",children:"placeholder"}),", ",(0,i.jsx)(n.a,{href:"https://testing-library.com/docs/queries/bytext",children:"text contents"}),", ",(0,i.jsx)(n.a,{href:"https://testing-library.com/docs/queries/bydisplayvalue",children:"display value"}),", ",(0,i.jsx)(n.a,{href:"https://testing-library.com/docs/queries/byalttext",children:"alt text"}),", ",(0,i.jsx)(n.a,{href:"https://testing-library.com/docs/queries/bytitle",children:"title"}),", ",(0,i.jsx)(n.a,{href:"https://testing-library.com/docs/queries/bytestid",children:"test ID"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"If testing with a database then make sure you control the data. If test are run against a staging environment make sure it doesn't change."}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"\u274c Don't:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Don't test implementation details. When refactoring code, tests shouldn't change."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Don't re-test the library/framework."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Don't mandate 100% code coverage for applications."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Don't test third-party dependencies. Only test what your team controls (package, API, microservice etc.). Don't test external sites links, third party servers, packages etc."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Don't test just to test."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nit('should render the user list', () => {\n render();\n expect(screen.getByText('Users List')).toBeInTheDocument();\n});\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"test-description",children:"Test Description"}),"\n",(0,i.jsxs)(n.p,{children:["All test descriptions must follow naming convention as ",(0,i.jsx)(n.code,{children:"it('should ... when ...')"}),"."]}),"\n",(0,i.jsx)(o.jO,{href:"https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/valid-title.md#mustmatch",children:"'vitest/valid-title': [\n 'error',\n {\n mustMatch: { it: [/should.*when/u.source, \"Test title must include 'should' and 'when'\"] },\n },\n]"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nit('accepts ISO date format where date is parsed and formatted as YYYY-MM');\nit('after title is confirmed user description is rendered');\n\n// \u2705 Name test description as it('should ... when ...')\nit('should return parsed date as YYYY-MM when input is in ISO date format');\nit('should render user description when title is confirmed');\n"})}),"\n",(0,i.jsx)(n.h3,{id:"test-tooling",children:"Test Tooling"}),"\n",(0,i.jsxs)(n.p,{children:["Besides running tests through scripts, it's highly encouraged to use ",(0,i.jsx)(n.a,{href:"https://marketplace.visualstudio.com/items?itemName=vitest.explorer",children:"Vitest Runner"})," and ",(0,i.jsx)(n.a,{href:"https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright",children:"Playwright Test"})," VS code extension alongside.",(0,i.jsx)(n.br,{}),"\n","With extension any single ",(0,i.jsx)(n.a,{href:"https://github.com/mkosir/typescript-style-guide/raw/main/misc/vscode-vitest-runner.gif",children:"unit/integration"})," or ",(0,i.jsx)(n.a,{href:"https://github.com/mkosir/typescript-style-guide/raw/main/misc/vscode-playwright-test.gif",children:"E2E"})," test can be run instantly, especially if testing app or package in larger monorepo codebase."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-sh",children:"code --install-extension vitest.explorer\ncode --install-extension ms-playwright.playwright\n"})}),"\n",(0,i.jsx)(n.h3,{id:"snapshot",children:"Snapshot"}),"\n",(0,i.jsxs)(n.p,{children:['Snapshot tests are discouraged in order to avoid fragility, which leads to "just update it" turn of mind, to achieve all the tests pass.',(0,i.jsx)(n.br,{}),"\n","Exceptions can be made, with strong rational behind it, where test output has short and clear intent, what's actually being tested (e.g. design system library critical elements that shouldn't deviate)."]})]})}function y(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(x,{...e})}):x(e)}},8453:(e,n,s)=>{s.d(n,{R:()=>o,x:()=>a});var t=s(6540);const i={},r=t.createContext(i);function o(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]);
\ No newline at end of file
diff --git a/assets/js/e7ce6630.8496596e.js b/assets/js/e7ce6630.8496596e.js
new file mode 100644
index 0000000..466dbaf
--- /dev/null
+++ b/assets/js/e7ce6630.8496596e.js
@@ -0,0 +1 @@
+"use strict";(self.webpackChunktypescript_style_guide_website=self.webpackChunktypescript_style_guide_website||[]).push([[490],{9558:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>u,contentTitle:()=>h,default:()=>y,frontMatter:()=>p,metadata:()=>t,toc:()=>m});const t=JSON.parse('{"type":"mdx","permalink":"/typescript-style-guide/","source":"@site/src/pages/index.mdx","title":"TypeScript Style Guide","description":"TypeScript Style Guide provides a concise set of conventions and best practices to create consistent, maintainable code.","frontMatter":{"title":"TypeScript Style Guide","description":"TypeScript Style Guide provides a concise set of conventions and best practices to create consistent, maintainable code.","toc_min_heading_level":2,"toc_max_heading_level":2},"unlisted":false}');var i=s(4848),r=s(8453),o=s(9003),a=s(6540);const l=e=>{const n=e.substring(1).replace(/---/g,"__dash__").replace(/--/g," & ").replace(/-/g," ").replace(/__dash__/g," - ");return c(n)},c=e=>e.toLowerCase().split(" ").map((e=>e[0]?.toUpperCase()+e.substring(1))).join(" "),d=e=>{let{children:n}=e;var s;return s=n,(0,a.useEffect)((()=>{const e=()=>{const e=window.location.hash;document.title=e?`${l(e)} | ${s}`:s};return e(),window.addEventListener("popstate",e),()=>{window.removeEventListener("popstate",e)}}),[s]),null},p={title:"TypeScript Style Guide",description:"TypeScript Style Guide provides a concise set of conventions and best practices to create consistent, maintainable code.",toc_min_heading_level:2,toc_max_heading_level:2},h=void 0,u={},m=[{value:"Introduction",id:"introduction",level:2},{value:"Table of Contents",id:"table-of-contents",level:2},{value:"About Guide",id:"about-guide",level:2},{value:"What",id:"what",level:3},{value:"Why",id:"why",level:3},{value:"Disclaimer",id:"disclaimer",level:3},{value:"Requirements",id:"requirements",level:3},{value:"TLDR",id:"tldr",level:2},{value:"Types",id:"types",level:2},{value:"Type Inference",id:"type-inference",level:3},{value:"Data Immutability",id:"data-immutability",level:3},{value:"Required & Optional Object Properties",id:"required--optional-object-properties",level:3},{value:"Discriminated Union",id:"discriminated-union",level:3},{value:"Return Types",id:"return-types",level:3},{value:"Type-Safe Constants with satisfies",id:"type-safe-constants-with-satisfies",level:3},{value:"Template Literal Types",id:"template-literal-types",level:3},{value:"Type any & unknown",id:"type-any--unknown",level:3},{value:"Type & Non-nullability Assertions",id:"type--non-nullability-assertions",level:3},{value:"Type Error",id:"type-error",level:3},{value:"Type Definition",id:"type-definition",level:3},{value:"Array Types",id:"array-types",level:3},{value:"Type Imports and Exports",id:"type-imports-and-exports",level:3},{value:"Services",id:"services",level:3},{value:"Functions",id:"functions",level:2},{value:"General",id:"general",level:3},{value:"Single Object Arg",id:"single-object-arg",level:3},{value:"Required & Optional Args",id:"required--optional-args",level:3},{value:"Args as Discriminated Type",id:"args-as-discriminated-type",level:3},{value:"Variables",id:"variables",level:2},{value:"Const Assertion",id:"const-assertion",level:3},{value:"Enums & Const Assertion",id:"enums--const-assertion",level:3},{value:"Type Union & Boolean Flags",id:"type-union--boolean-flags",level:3},{value:"Null & Undefined",id:"null--undefined",level:3},{value:"Naming",id:"naming",level:2},{value:"Named Export",id:"named-export",level:3},{value:"Naming Conventions",id:"naming-conventions",level:3},{value:"Variables",id:"variables-1",level:4},{value:"Functions",id:"functions-1",level:4},{value:"Types",id:"types-1",level:4},{value:"Generics",id:"generics",level:4},{value:"Abbreviations & Acronyms",id:"abbreviations--acronyms",level:4},{value:"React Components",id:"react-components",level:4},{value:"Prop Types",id:"prop-types",level:4},{value:"Callback Props",id:"callback-props",level:4},{value:"React Hooks",id:"react-hooks",level:4},{value:"Comments",id:"comments",level:3},{value:"Source Organization",id:"source-organization",level:2},{value:"Code Collocation",id:"code-collocation",level:3},{value:"Imports",id:"imports",level:3},{value:"Project Structure",id:"project-structure",level:3},{value:"Appendix - React",id:"appendix---react",level:2},{value:"Required & Optional Props",id:"required--optional-props",level:3},{value:"Props as Discriminated Type",id:"props-as-discriminated-type",level:3},{value:"Props To State",id:"props-to-state",level:3},{value:"Props Type",id:"props-type",level:3},{value:"Component Types",id:"component-types",level:3},{value:"Container",id:"container",level:4},{value:"UI - Feature",id:"ui---feature",level:4},{value:"UI - Design system",id:"ui---design-system",level:4},{value:"Store & Pass Data",id:"store--pass-data",level:3},{value:"Appendix - Tests",id:"appendix---tests",level:2},{value:"What & How To Test",id:"what--how-to-test",level:3},{value:"Test Description",id:"test-description",level:3},{value:"Test Tooling",id:"test-tooling",level:3},{value:"Snapshot",id:"snapshot",level:3}];function x(e){const n={a:"a",br:"br",code:"code",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(d,{children:"TypeScript Style Guide"}),"\n",(0,i.jsx)(o.OB,{children:"TypeScript Style Guide"}),"\n",(0,i.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,i.jsx)(n.p,{children:"TypeScript Style Guide provides a concise set of conventions and best practices to create consistent, maintainable code."}),"\n",(0,i.jsx)(n.h2,{id:"table-of-contents",children:"Table of Contents"}),"\n",(0,i.jsx)(o.MB,{items:m}),"\n",(0,i.jsx)(n.h2,{id:"about-guide",children:"About Guide"}),"\n",(0,i.jsx)(n.h3,{id:"what",children:"What"}),"\n",(0,i.jsxs)(n.p,{children:['Since "consistency is the key", TypeScript Style Guide strives to enforce majority of the rules by using automated tooling as ESLint, TypeScript, Prettier, etc.',(0,i.jsx)(n.br,{}),"\n","Still certain design and architectural decisions must be followed which are described with conventions below."]}),"\n",(0,i.jsx)(n.h3,{id:"why",children:"Why"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"As project grow in size and complexity, maintaining code quality and ensuring consistent practices become increasingly challenging."}),"\n",(0,i.jsx)(n.li,{children:"Defining and following a standard way to write TypeScript applications brings a consistent codebase and faster development cycles."}),"\n",(0,i.jsx)(n.li,{children:"No need to discuss code styles in code reviews."}),"\n",(0,i.jsx)(n.li,{children:"Saves team time and energy."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"disclaimer",children:"Disclaimer"}),"\n",(0,i.jsx)(n.p,{children:"As any code style guide is opinionated, this is no different as it tries to set conventions (sometimes arbitrary) that govern our code."}),"\n",(0,i.jsx)(n.p,{children:"You don't have to follow every convention exactly as written in the guide, decide what works best for your product and team to stay consistent with your codebase."}),"\n",(0,i.jsx)(n.h3,{id:"requirements",children:"Requirements"}),"\n",(0,i.jsx)(n.p,{children:"Style Guide requires you to use:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"https://github.com/microsoft/TypeScript",children:"TypeScript v5"})}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://github.com/typescript-eslint/typescript-eslint",children:"typescript-eslint v7"})," with ",(0,i.jsx)(n.a,{href:"https://typescript-eslint.io/linting/configs/#strict-type-checked",children:(0,i.jsx)(n.code,{children:"strict-type-checked"})})," configuration enabled."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Style Guide assumes using, but is not limited to:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://github.com/facebook/react",children:"React"})," as UI library for frontend conventions."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.a,{href:"https://playwright.dev/",children:"Playwright"})," and ",(0,i.jsx)(n.a,{href:"https://vitest.dev/",children:"Vitest"})," for testing conventions."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"tldr",children:"TLDR"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Embrace const assertions"}),". ",(0,i.jsx)(n.a,{href:"#const-assertion",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Strive for ",(0,i.jsx)(n.strong,{children:"data immutability"})," using types like ",(0,i.jsx)(n.code,{children:"Readonly"})," and ",(0,i.jsx)(n.code,{children:"ReadonlyArray"}),". ",(0,i.jsx)(n.a,{href:"#data-immutability",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Majority of ",(0,i.jsx)(n.strong,{children:"object properties"})," should be ",(0,i.jsx)(n.strong,{children:"required"})," (use optional sparingly). ",(0,i.jsx)(n.a,{href:"#required--optional-object-properties",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Embrace discriminated unions"}),". ",(0,i.jsx)(n.a,{href:"#discriminated-union",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Avoid type assertions"}),". ",(0,i.jsx)(n.a,{href:"#type--non-nullability-assertions",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Strive for functions to be ",(0,i.jsx)(n.strong,{children:"pure"}),", ",(0,i.jsx)(n.strong,{children:"stateless"})," and have ",(0,i.jsx)(n.strong,{children:"single responsibility"}),". ",(0,i.jsx)(n.a,{href:"#functions",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Strong emphasis to keep ",(0,i.jsx)(n.strong,{children:"naming conventions consistent and readable"}),". ",(0,i.jsx)(n.a,{href:"#naming-conventions",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Use ",(0,i.jsx)(n.strong,{children:"named exports"}),". ",(0,i.jsx)(n.a,{href:"#named-export",children:"\u2b63"})]}),"\n",(0,i.jsxs)(n.li,{children:["Code is ",(0,i.jsx)(n.strong,{children:"organized"})," and ",(0,i.jsx)(n.strong,{children:"grouped by feature"}),". Collocate code as close as possible to where it's relevant. ",(0,i.jsx)(n.a,{href:"#code-collocation",children:"\u2b63"})]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"types",children:"Types"}),"\n",(0,i.jsxs)(n.p,{children:["When creating types we try to think of how they would best ",(0,i.jsx)(n.strong,{children:"describe our code"}),".",(0,i.jsx)(n.br,{}),"\n","Being expressive and keeping types as ",(0,i.jsx)(n.strong,{children:"narrow as possible"})," brings benefits to the codebase:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Increased Type Safety - Catch errors at compile-time, since narrowed types provide more specific information about the shape and behavior of your data."}),"\n",(0,i.jsx)(n.li,{children:"Improved Code Clarity - Cognitive load is reduced by providing clearer boundaries and constraints on your data which makes your code easier to understand by other developers."}),"\n",(0,i.jsx)(n.li,{children:"Easier Refactoring - Refactor with confidence, since types are narrow, making changes to your code becomes less risky."}),"\n",(0,i.jsx)(n.li,{children:"Optimized Performance - In some cases, narrow types can help the TypeScript compiler generate more optimized JavaScript code."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"type-inference",children:"Type Inference"}),"\n",(0,i.jsx)(n.p,{children:"As rule of thumb, explicitly declare a type when it help narrows it."}),"\n",(0,i.jsx)(o.L7,{children:(0,i.jsx)(n.p,{children:"Just because you don't need to add types, doesn't mean you shouldn't. In some cases explicit type declaration can\nincrease code readability and intent."})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid - Don't explicitly declare a type, it can be inferred.\nconst userRole: string = 'admin'; // Type 'string'\nconst employees = new Map([['Gabriel', 32]]);\nconst [isActive, setIsActive] = useState(false);\n\n// \u2705 Use type inference.\nconst USER_ROLE = 'admin'; // Type 'admin'\nconst employees = new Map([['Gabriel', 32]]); // Type 'Map'\nconst [isActive, setIsActive] = useState(false); // Type 'boolean'\n\n// \u274c Avoid - Don't infer a (wide) type, it can be narrowed.\nconst employees = new Map(); // Type 'Map'\nemployees.set('Lea', 17);\ntype UserRole = 'admin' | 'guest';\nconst [userRole, setUserRole] = useState('admin'); // Type 'string'\n\n// \u2705 Use explicit type declaration to narrow the type.\nconst employees = new Map(); // Type 'Map'\nemployees.set('Gabriel', 32);\ntype UserRole = 'admin' | 'guest';\nconst [userRole, setUserRole] = useState('admin'); // Type 'UserRole'\n"})}),"\n",(0,i.jsx)(n.h3,{id:"data-immutability",children:"Data Immutability"}),"\n",(0,i.jsxs)(n.p,{children:["The majority of the data should be immutable, using types like ",(0,i.jsx)(n.code,{children:"Readonly"})," and ",(0,i.jsx)(n.code,{children:"ReadonlyArray"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Using readonly type prevents accidental data mutations and reduces the risk of bugs caused by unintended side effects."}),"\n",(0,i.jsxs)(n.p,{children:["When performing data processing always return new array, object etc. To minimize cognitive load for future developers, aim to keep data objects flat and small.",(0,i.jsx)(n.br,{}),"\n","Mutations should be used sparingly and only as an exception in cases where they are truly necessary, such as when dealing with complex objects, performance-related reasons etc."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid data mutations\nconst removeFirstUser = (users: Array) => {\n if (users.length === 0) {\n return users;\n }\n return users.splice(1);\n};\n\n// \u2705 Use readonly type to prevent accidental mutations\nconst removeFirstUser = (users: ReadonlyArray) => {\n if (users.length === 0) {\n return users;\n }\n return users.slice(1);\n // Using arr.splice(1) errors - Function 'splice' does not exist on 'users'\n};\n"})}),"\n",(0,i.jsx)(n.h3,{id:"required--optional-object-properties",children:"Required & Optional Object Properties"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Strive to have majority of object properties required and use optional sparingly."})}),"\n",(0,i.jsx)(n.p,{children:"It will reflect designing type-safe and maintainable code:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Clarity and Predictability - Required properties make it explicit which data is always expected. This reduces ambiguity for developers using or consuming the object, as they know exactly what must be present."}),"\n",(0,i.jsx)(n.li,{children:"Type Safety - When properties are required, TypeScript can enforce their presence at compile time. This prevents runtime errors caused by missing properties."}),"\n",(0,i.jsxs)(n.li,{children:["Avoids Overuse of Optional Chaining - If too many properties are optional, it often leads to extensive use of optional chaining (",(0,i.jsx)(n.code,{children:"?."}),") to handle potential undefined values. This clutters the code and obscure its intent."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["If introduction of many optional properties truly can't be avoided utilize ",(0,i.jsx)(n.strong,{children:"discriminated union type"}),"."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid optional properties as they increase complexity\ntype User = {\n id?: number;\n email?: string;\n dashboardAccess?: boolean;\n adminPermissions?: ReadonlyArray;\n subscriptionPlan?: 'free' | 'pro' | 'premium';\n rewardsPoints?: number;\n temporaryToken?: string;\n};\n\n// \u2705 Strive to have majority of properties required, if that's not possible,\n// use discriminated union for clear intent on object usage\ntype AdminUser = {\n role: 'admin';\n id: number;\n email: string;\n dashboardAccess: boolean;\n adminPermissions: ReadonlyArray;\n};\n\ntype RegularUser = {\n role: 'regular';\n id: number;\n email: string;\n subscriptionPlan: 'free' | 'pro' | 'premium';\n rewardsPoints: number;\n};\n\ntype GuestUser = {\n role: 'guest';\n temporaryToken: string;\n};\n\n// Discriminated union type 'User' with no optional properties\ntype User = AdminUser | RegularUser | GuestUser;\n\nconst regularUser: User = {\n role: 'regular',\n id: 212,\n email: 'lea@user.com',\n subscriptionPlan: 'pro',\n rewardsPoints: 1500,\n dashboardAccess: false, // Error 'dashboardAccess' property does not exist\n};\n"})}),"\n",(0,i.jsx)(n.h3,{id:"discriminated-union",children:"Discriminated Union"}),"\n",(0,i.jsx)(n.p,{children:"If there's only one TypeScript feature to choose from, embrace discriminated unions."}),"\n",(0,i.jsxs)(n.p,{children:["Discriminated unions are a powerful concept to model complex data structures and improve type safety, leading to clearer and less error-prone code.",(0,i.jsx)(n.br,{}),"\n","You may encounter discriminated unions under different names such as tagged unions or sum types in various programming languages as C, Haskell, Rust (in conjunction with pattern-matching)."]}),"\n",(0,i.jsx)(n.p,{children:"Discriminated unions advantages:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["As mentioned in ",(0,i.jsx)(n.a,{href:"#required--optional-object-properties",children:"Required & Optional Object Properties"}),", ",(0,i.jsx)(n.a,{href:"#args-as-discriminated-type",children:"Args as Discriminated Union"})," and ",(0,i.jsx)(n.a,{href:"#props-as-discriminated-type",children:"Props as Discriminated Type"}),", discriminated union eliminates optional object properties which decreases complexity."]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Exhaustiveness check - TypeScript can ensure that all possible variants of a type are implemented, eliminating the risk of undefined or unexpected behavior at runtime."}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/switch-exhaustiveness-check/",children:'"@typescript-eslint/switch-exhaustiveness-check": "error"'}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"type Circle = { kind: 'circle'; radius: number };\ntype Square = { kind: 'square'; size: number };\ntype Triangle = { kind: 'triangle'; base: number; height: number };\n\n// Create discriminated union 'Shape', with 'kind' property to discriminate the type of object.\ntype Shape = Circle | Square | Triangle;\n\n// TypeScript warns us with errors in calculateArea function\nconst calculateArea = (shape: Shape) => {\n // Error - Switch is not exhaustive. Cases not matched: \"triangle\"\n switch (shape.kind) {\n case 'circle':\n return Math.PI * shape.radius ** 2;\n case 'square':\n return shape.size * shape.width; // Error - Property 'width' does not exist on type 'square'\n }\n};\n"})}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["Avoid code complexity introduced by ",(0,i.jsx)(n.a,{href:"#type-union--boolean-flags",children:"flag variables"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Clear code intent, as it becomes easier to read and understand by explicitly indicating the possible cases for a given type."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"TypeScript can narrow down union types, ensuring code correctness at compile time."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Discriminated unions make refactoring and maintenance easier by providing a centralized definition of related types. When adding or modifying types within the union, the compiler reports any inconsistencies throughout the codebase."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"IDEs can leverage discriminated unions to provide better autocompletion and type inference."}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"return-types",children:"Return Types"}),"\n",(0,i.jsx)(o.jO,{prefix:"Including return type annotations is highly encouraged, although not required",href:"https://typescript-eslint.io/rules/explicit-function-return-type/",children:'"@typescript-eslint/explicit-function-return-type": "error"'}),"\n",(0,i.jsx)(n.p,{children:"Consider benefits when explicitly typing the return value of a function:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Return values makes it clear and easy to understand to any calling code what type is returned."}),"\n",(0,i.jsx)(n.li,{children:"In the case where there is no return value, the calling code doesn't try to use the undefined value when it shouldn't."}),"\n",(0,i.jsx)(n.li,{children:"Surface potential type errors faster in the future if there are code changes that change the return type of the function."}),"\n",(0,i.jsx)(n.li,{children:"Easier to refactor, since it ensures that the return value is assigned to a variable of the correct type."}),"\n",(0,i.jsx)(n.li,{children:"Similar to writing tests before implementation (TDD), defining function arguments and return type, gives you the opportunity to discuss the feature functionality and its interface ahead of implementation."}),"\n",(0,i.jsx)(n.li,{children:"Although type inference is very convenient, adding return types can save TypeScript compiler a lot of work."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"type-safe-constants-with-satisfies",children:"Type-Safe Constants with satisfies"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"as const satisfies"})," syntax is a powerful TypeScript feature that combines strict type-checking and immutability for constants. It is particularly useful when defining constants that need to conform to a specific type."]}),"\n",(0,i.jsx)(n.p,{children:"Key benefits:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Immutability with ",(0,i.jsx)(n.code,{children:"as const"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Ensures the constant is treated as readonly."}),"\n",(0,i.jsx)(n.li,{children:"Narrows the types of values to their literals, preventing accidental modifications."}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["Validation with ",(0,i.jsx)(n.code,{children:"satisfies"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Ensures the object conforms to a broader type without widening its inferred type."}),"\n",(0,i.jsx)(n.li,{children:"Helps catch type mismatches at compile time while preserving narrowed inferred types."}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// Array constant\ntype UserRole = 'admin' | 'editor' | 'moderator' | 'viewer' | 'guest';\n\n// \u274c Avoid constant of wide type\nconst DASHBOARD_ACCESS_ROLES: ReadonlyArray = ['admin', 'editor', 'moderator'];\n\n// \u274c Avoid constant with incorrect values\nconst DASHBOARD_ACCESS_ROLES = ['admin', 'contributor', 'analyst'] as const;\n\n// \u2705 Use immutable constant of narrowed type\nconst DASHBOARD_ACCESS_ROLES = ['admin', 'editor', 'moderator'] as const satisfies ReadonlyArray;\n\n// Object constant\ntype OrderStatus = {\n pending: 'pending' | 'idle';\n fulfilled: boolean;\n error: string;\n};\n\n// \u274c Avoid mutable constant of wide type\nconst IDLE_ORDER: OrderStatus = {\n pending: 'idle',\n fulfilled: true,\n error: 'Shipping Error',\n};\n\n// \u274c Avoid constant with incorrect values\nconst IDLE_ORDER = {\n pending: 'done',\n fulfilled: 'partially',\n error: 116,\n} as const;\n\n// \u2705 Use immutable constant of narrowed type\nconst IDLE_ORDER = {\n pending: 'idle',\n fulfilled: true,\n error: 'Shipping Error',\n} as const satisfies OrderStatus;\n"})}),"\n",(0,i.jsx)(n.h3,{id:"template-literal-types",children:"Template Literal Types"}),"\n",(0,i.jsxs)(n.p,{children:["Embrace template literal types instead of using the broad ",(0,i.jsx)(n.code,{children:"string"})," type wherever possible.\nTemplate literal types have many practical use cases, such as defining API endpoints, routes, internationalization, database queries, CSS typings etc."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst userEndpoint = '/api/usersss'; // Type 'string' - Typo 'usersss': the route doesn't exist, leading to a runtime error.\n// \u2705 Use\ntype ApiRoute = 'users' | 'posts' | 'comments';\ntype ApiEndpoint = `/api/${ApiRoute}`; // Type ApiEndpoint = \"/api/users\" | \"/api/posts\" | \"/api/comments\"\nconst userEndpoint: ApiEndpoint = '/api/users';\n\n// \u274c Avoid\nconst homeTitle = 'translation.homesss.title'; // Type 'string' - Typo 'homesss': the translation doesn't exist, leading to a runtime error.\n// \u2705 Use\ntype LocaleKeyPages = 'home' | 'about' | 'contact';\ntype TranslationKey = `translation.${LocaleKeyPages}.${string}`; // Type TranslationKey = `translation.home.${string}` | `translation.about.${string}` | `translation.contact.${string}`\nconst homeTitle: TranslationKey = 'translation.home.title';\n\n// \u274c Avoid\nconst color = 'blue-450'; // Type 'string' - Color 'blue-450' doesn't exist, leading to a runtime error.\n// \u2705 Use\ntype BaseColor = 'blue' | 'red' | 'yellow' | 'gray';\ntype Variant = 50 | 100 | 200 | 300 | 400;\ntype Color = `${BaseColor}-${Variant}` | `#${string}`; // Type Color = \"blue-50\" | \"blue-100\" | \"blue-200\" ... | \"red-50\" | \"red-100\" ... | #${string}\nconst iconColor: Color = 'blue-400';\nconst customColor: Color = '#AD3128';\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type-any--unknown",children:"Type any & unknown"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"any"})," data type must not be used as it represents literally \u201cany\u201d value that TypeScript defaults to and skips type checking since it cannot infer the type. As such, ",(0,i.jsx)(n.code,{children:"any"})," is dangerous, it can mask severe programming errors."]}),"\n",(0,i.jsxs)(n.p,{children:["When dealing with ambiguous data type use ",(0,i.jsx)(n.code,{children:"unknown"}),", which is the type-safe counterpart of ",(0,i.jsx)(n.code,{children:"any"}),".",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"unknown"})," doesn't allow dereferencing all properties (anything can be assigned to ",(0,i.jsx)(n.code,{children:"unknown"}),", but ",(0,i.jsx)(n.code,{children:"unknown"})," isn\u2019t assignable to anything)."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid any\nconst foo: any = 'five';\nconst bar: number = foo; // no type error\n\n// \u2705 Use unknown\nconst foo: unknown = 5;\nconst bar: number = foo; // type error - Type 'unknown' is not assignable to type 'number'\n\n// Narrow the type before dereferencing it using:\n// Type guard\nconst isNumber = (num: unknown): num is number => {\n return typeof num === 'number';\n};\nif (!isNumber(foo)) {\n throw Error(`API provided a fault value for field 'foo':${foo}. Should be a number!`);\n}\nconst bar: number = foo;\n\n// Type assertion\nconst bar: number = foo as number;\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type--non-nullability-assertions",children:"Type & Non-nullability Assertions"}),"\n",(0,i.jsxs)(n.p,{children:["Type assertions ",(0,i.jsx)(n.code,{children:"user as User"})," and non-nullability assertions ",(0,i.jsx)(n.code,{children:"user!.name"})," are unsafe. Both only silence TypeScript compiler and increase the risk of crashing application at runtime.",(0,i.jsx)(n.br,{}),"\n","They can only be used as an exception (e.g. third party library types mismatch, dereferencing ",(0,i.jsx)(n.code,{children:"unknown"})," etc.) with a strong rational for why it's introduced into the codebase."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"type User = { id: string; username: string; avatar: string | null };\n// \u274c Avoid type assertions\nconst user = { name: 'Nika' } as User;\n// \u274c Avoid non-nullability assertions\nrenderUserAvatar(user!.avatar); // Runtime error\n\nconst renderUserAvatar = (avatar: string) => {...}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type-error",children:"Type Error"}),"\n",(0,i.jsxs)(n.p,{children:["When a TypeScript error cannot be mitigated, use ",(0,i.jsx)(n.code,{children:"@ts-expect-error"})," as a last resort to suppress it. This directive enables the TypeScript compiler to indicate when the suppressed line no longer contains an error, ensuring that suppressed errors are revisited when they are no longer relevant."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Always use ",(0,i.jsx)(n.code,{children:"@ts-expect-error"})," with a clear description explaining why it is necessary."]}),"\n",(0,i.jsxs)(n.li,{children:["The use of ",(0,i.jsx)(n.code,{children:"@ts-ignore"})," should be avoided, as it does not provide the same level of safety and accountability as ",(0,i.jsx)(n.code,{children:"@ts-expect-error"}),"."]}),"\n"]}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/ban-ts-comment/#allow-with-description",children:"'@typescript-eslint/ban-ts-comment': [\n'error',\n{\n 'ts-expect-error': 'allow-with-description'\n},\n]"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid @ts-ignore as it will do nothing if the following line is error-free.\n// @ts-ignore\nconst newUser = createUser('Gabriel');\n\n// \u2705 Use @ts-expect-error with description.\n// @ts-expect-error: This library function has incorrect type definitions - createUser accepts string as an argument.\nconst newUser = createUser('Gabriel');\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type-definition",children:"Type Definition"}),"\n",(0,i.jsxs)(n.p,{children:["TypeScript offers two options for type definitions - ",(0,i.jsx)(n.code,{children:"type"})," and ",(0,i.jsx)(n.code,{children:"interface"}),". As they come with some functional differences in most cases they can be used interchangeably. We try to limit syntax difference and pick one for consistency."]}),"\n",(0,i.jsx)(o.jO,{prefix:"All types must be defined with `type` alias",href:"https://typescript-eslint.io/rules/consistent-type-definitions/#type",children:"'@typescript-eslint/consistent-type-definitions': ['error', 'type']"}),"\n",(0,i.jsx)(o.L7,{children:(0,i.jsxs)(n.p,{children:["Consider using interfaces when developing a package that may be extended in the future by third-party consumers or\nwhen the team prefers working with interfaces. In such cases, you can disable linting rules where necessary, such as\nwhen using type unions (e.g. ",(0,i.jsx)(n.code,{children:"type Status = 'loading' | 'error'"}),")."]})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid interface definitions\ninterface UserRole = 'admin' | 'guest'; // invalid - interface can't define (commonly used) type unions\n\ninterface UserInfo {\n name: string;\n role: 'admin' | 'guest';\n}\n\n// \u2705 Use type definition\ntype UserRole = 'admin' | 'guest';\n\ntype UserInfo = {\n name: string;\n role: UserRole;\n};\n\n"})}),"\n",(0,i.jsxs)(n.p,{children:["When performing declaration merging (e.g. extending third-party library types), use ",(0,i.jsx)(n.code,{children:"interface"})," and disable the lint rule where necessary."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// types.ts\ndeclare namespace NodeJS {\n // eslint-disable-next-line @typescript-eslint/consistent-type-definitions\n export interface ProcessEnv {\n NODE_ENV: 'development' | 'production';\n PORT: string;\n CUSTOM_ENV_VAR: string;\n }\n}\n\n// server.ts\napp.listen(process.env.PORT, () => {...}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"array-types",children:"Array Types"}),"\n",(0,i.jsx)(o.jO,{prefix:"Array types should be defined using generic syntax",href:"https://typescript-eslint.io/rules/array-type/#generic",children:"'@typescript-eslint/array-type': ['error', { default: 'generic' }]"}),"\n",(0,i.jsx)(o.L7,{children:(0,i.jsx)(n.p,{children:"Since there is no functional difference between the 'generic' and 'array' definitions, feel free to choose the one\nthat your team finds most readable."})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst x: string[] = ['foo', 'bar'];\nconst y: readonly string[] = ['foo', 'bar'];\n\n// \u2705 Use\nconst x: Array = ['foo', 'bar'];\nconst y: ReadonlyArray = ['foo', 'bar'];\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type-imports-and-exports",children:"Type Imports and Exports"}),"\n",(0,i.jsxs)(n.p,{children:["TypeScript allows specifying a ",(0,i.jsx)(n.code,{children:"type"})," keyword on imports to indicate that the export exists only in the type system, not at runtime."]}),"\n",(0,i.jsx)(n.p,{children:"Type imports must always be separated:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Tree Shaking and Dead Code Elimination: If you use ",(0,i.jsx)(n.code,{children:"import"})," for types instead of ",(0,i.jsx)(n.code,{children:"import type"}),", the bundler might include the imported module in the bundle unnecessarily, increasing the size. Separating imports ensures that only necessary runtime code is included."]}),"\n",(0,i.jsx)(n.li,{children:"Minimizing Dependencies: Some modules may contain both runtime and type definitions. Mixing type imports with runtime imports might lead to accidental inclusion of unnecessary runtime code."}),"\n",(0,i.jsx)(n.li,{children:"Improves code clarity by making the distinction between runtime dependencies and type-only imports explicit."}),"\n"]}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/consistent-type-imports/",children:"'@typescript-eslint/consistent-type-imports': 'error'"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid using `import` for both runtime and type\nimport { MyClass } from 'some-library';\n\n// Even if MyClass is only a type, the entire module might be included in the bundle.\n\n// \u2705 Use `import type`\nimport type { MyClass } from 'some-library';\n\n// This ensures only the type is imported and no runtime code from \"some-library\" ends up in the bundle.\n"})}),"\n",(0,i.jsx)(n.h3,{id:"services",children:"Services"}),"\n",(0,i.jsx)(n.p,{children:"Documentation becomes outdated the moment it's written, and worse than no documentation is wrong documentation. The same applies to types when describing the modules your app interacts with, such as APIs, messaging systems, databases etc."}),"\n",(0,i.jsxs)(n.p,{children:["For external API services, such as REST, GraphQL etc. it's crucial to generate types from their contracts, whether they use Swagger, schemas, or other sources (e.g. ",(0,i.jsx)(n.a,{href:"https://github.com/drwpow/openapi-typescript",children:"openapi-ts"}),", ",(0,i.jsx)(n.a,{href:"https://github.com/kamilkisiela/graphql-config",children:"graphql-config"})," etc.). Avoid manually declaring and maintaining types, as they can easily fall out of sync."]}),"\n",(0,i.jsx)(n.p,{children:"As an exception manually declare types only when there is truly no documentation provided by external service."}),"\n",(0,i.jsx)(n.h2,{id:"functions",children:"Functions"}),"\n",(0,i.jsx)(n.p,{children:"Function conventions should be followed as much as possible (some of the conventions derive from functional programming basic concepts):"}),"\n",(0,i.jsx)(n.h3,{id:"general",children:"General"}),"\n",(0,i.jsx)(n.p,{children:"Function:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"should have single responsibility."}),"\n",(0,i.jsx)(n.li,{children:"should be stateless where the same input arguments return same value every single time."}),"\n",(0,i.jsx)(n.li,{children:"should accept at least one argument and return data."}),"\n",(0,i.jsx)(n.li,{children:"should not have side effects, but be pure. Implementation should not modify or access variable value outside its local environment (global state, fetching etc.)."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"single-object-arg",children:"Single Object Arg"}),"\n",(0,i.jsxs)(n.p,{children:["To keep function readable and easily extensible for the future (adding/removing args), strive to have single object as the function arg, instead of multiple args.",(0,i.jsx)(n.br,{}),"\n","As an exception this does not apply when having only one primitive single arg (e.g. simple functions isNumber(value), implementing currying etc.)."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid having multiple arguments\ntransformUserInput('client', false, 60, 120, null, true, 2000);\n\n// \u2705 Use options object as argument\ntransformUserInput({\n method: 'client',\n isValidated: false,\n minLines: 60,\n maxLines: 120,\n defaultInput: null,\n shouldLog: true,\n timeout: 2000,\n});\n"})}),"\n",(0,i.jsx)(n.h3,{id:"required--optional-args",children:"Required & Optional Args"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Strive to have majority of args required and use optional sparingly."}),(0,i.jsx)(n.br,{}),"\n","If the function becomes too complex, it probably should be broken into smaller pieces.",(0,i.jsx)(n.br,{}),"\n",'An exaggerated example where implementing 10 functions with 5 required args each, is better then implementing one "can do it all" function that accepts 50 optional args.']}),"\n",(0,i.jsx)(n.h3,{id:"args-as-discriminated-type",children:"Args as Discriminated Type"}),"\n",(0,i.jsxs)(n.p,{children:["When applicable use ",(0,i.jsx)(n.strong,{children:"discriminated union type"})," to eliminate optional properties, which will decrease complexity on function API and only required properties will be passed depending on its use case."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid optional properties as they increase complexity of function API\ntype StatusParams = {\n data?: Products;\n title?: string;\n time?: number;\n error?: string;\n};\n\n// \u2705 Strive to have majority of properties required, if that's not possible,\n// use discriminated union for clear intent on function usage\ntype StatusSuccessParams = {\n status: 'success';\n data: Products;\n title: string;\n};\n\ntype StatusLoadingParams = {\n status: 'loading';\n time: number;\n};\n\ntype StatusErrorParams = {\n status: 'error';\n error: string;\n};\n\n// Discriminated function param 'StatusParams' with no optional properties\ntype StatusParams = StatusSuccessParams | StatusLoadingParams | StatusErrorParams;\n\nexport const parseStatus = (params: StatusParams) => {...\n"})}),"\n",(0,i.jsx)(n.h2,{id:"variables",children:"Variables"}),"\n",(0,i.jsx)(n.h3,{id:"const-assertion",children:"Const Assertion"}),"\n",(0,i.jsxs)(n.p,{children:["Strive to use const assertion ",(0,i.jsx)(n.code,{children:"as const"}),":"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"type is narrowed"}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["object gets ",(0,i.jsx)(n.code,{children:"readonly"})," properties"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["array becomes ",(0,i.jsx)(n.code,{children:"readonly"})," tuple"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid declaring constants without const assertion\nconst FOO_LOCATION = { x: 50, y: 130 }; // Type { x: number; y: number; }\nFOO_LOCATION.x = 10;\n\nconst BAR_LOCATION = [50, 130]; // Type number[]\nBAR_LOCATION.push(10);\n\nconst RATE_LIMIT = 25;\nconst RATE_LIMIT_MESSAGE = `Max number of requests/min is ${RATE_LIMIT}.`; // Type string\n\n// \u2705 Use const assertion\nconst FOO_LOCATION = { x: 50, y: 130 } as const; // Type '{ readonly x: 50; readonly y: 130; }'\nFOO_LOCATION.x = 10; // Error\n\nconst BAR_LOCATION = [50, 130] as const; // Type 'readonly [10, 20]'\nBAR_LOCATION.push(10); // Error\n\nconst RATE_LIMIT = 25;\nconst RATE_LIMIT_MESSAGE = `Max number of requests/min is ${RATE_LIMIT}.` as const; // Type 'Rate limit exceeded! Max number of requests/min is 25.'\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"enums--const-assertion",children:"Enums & Const Assertion"}),"\n",(0,i.jsx)(n.p,{children:"Const assertion must be used over enum."}),"\n",(0,i.jsxs)(n.p,{children:["While enums can still cover use cases as const assertion would, we tend to avoid it. Some of the reasonings as mentioned in TypeScript documentation - ",(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls",children:"Const enum pitfalls"}),", ",(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums",children:"Objects vs Enums"}),", ",(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings",children:"Reverse mappings"})," etc."]}),"\n",(0,i.jsx)(o.jO,{href:"https://eslint.org/docs/latest/rules/no-restricted-syntax",children:"'no-restricted-syntax': [\n 'error',\n {\n selector: 'TSEnumDeclaration',\n message: 'Use const assertion or a string union type instead.',\n },\n]"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid using enums\nenum UserRole {\n GUEST,\n MODERATOR,\n ADMINISTRATOR,\n}\n\nenum Color {\n PRIMARY = '#B33930',\n SECONDARY = '#113A5C',\n BRAND = '#9C0E7D',\n}\n\n// \u2705 Use const assertion\nconst USER_ROLES = ['guest', 'moderator', 'administrator'] as const;\ntype UserRole = (typeof USER_ROLES)[number]; // Type \"guest\" | \"moderator\" | \"administrator\"\n\n// Use satisfies if UserRole type is already defined - e.g. database schema model\ntype UserRoleDB = ReadonlyArray<'guest' | 'moderator' | 'administrator'>;\nconst AVAILABLE_ROLES = ['guest', 'moderator'] as const satisfies UserRoleDB;\n\nconst COLOR = {\n primary: '#B33930',\n secondary: '#113A5C',\n brand: '#9C0E7D',\n} as const;\ntype Color = typeof COLOR;\ntype ColorKey = keyof Color; // Type \"primary\" | \"secondary\" | \"brand\"\ntype ColorValue = Color[ColorKey]; // Type \"#B33930\" | \"#113A5C\" | \"#9C0E7D\"\n"})}),"\n",(0,i.jsx)(n.h3,{id:"type-union--boolean-flags",children:"Type Union & Boolean Flags"}),"\n",(0,i.jsx)(n.p,{children:"Embrace type unions, especially when type union options are mutually exclusive, instead multiple boolean flag variables."}),"\n",(0,i.jsx)(n.p,{children:"Boolean flags have a tendency to accumulate over time, leading to confusing and error-prone code, since they hide the actual app state."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid introducing multiple boolean flag variables\nconst isPending, isProcessing, isConfirmed, isExpired;\n\n// \u2705 Use type union variable\ntype UserStatus = 'pending' | 'processing' | 'confirmed' | 'expired';\nconst userStatus: UserStatus;\n"})}),"\n",(0,i.jsxs)(n.p,{children:["When boolean flags are used and the number of possible states grows quickly, it often results in unhandled or ambiguous states. Instead, take advantage of ",(0,i.jsx)(n.a,{href:"#discriminated-union",children:"discriminated unions"})," to better manage and represent your application's state."]}),"\n",(0,i.jsx)(n.h3,{id:"null--undefined",children:"Null & Undefined"}),"\n",(0,i.jsxs)(n.p,{children:["In TypeScript types ",(0,i.jsx)(n.code,{children:"null"})," and ",(0,i.jsx)(n.code,{children:"undefined"})," many times can be used interchangeably.",(0,i.jsx)(n.br,{}),"\n","Strive to:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Use ",(0,i.jsx)(n.code,{children:"null"})," to explicitly state it has no value - assignment, return function type etc."]}),"\n",(0,i.jsxs)(n.li,{children:["Use ",(0,i.jsx)(n.code,{children:"undefined"})," assignment when the value doesn't exist. E.g. exclude fields in form, request payload, database query (",(0,i.jsx)(n.a,{href:"https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined",children:"Prisma differentiation"}),") etc."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"naming",children:"Naming"}),"\n",(0,i.jsx)(n.p,{children:"Strive to keep naming conventions consistent and readable, with important context provided, because another person will maintain the code you have written."}),"\n",(0,i.jsx)(n.h3,{id:"named-export",children:"Named Export"}),"\n",(0,i.jsx)(o.jO,{prefix:"Named exports must be used to ensure that all imports follow a uniform pattern",href:"https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-default-export.md",children:'\'import/no-default-export\': \'error\'\n\n// In case of exceptions disable the rule\noverrides: [\n{\nfiles: ["src/pages/**/*"],\nrules: { "import/no-default-export": "off" },\n}\n]\n'}),"\n",(0,i.jsx)(n.p,{children:"This keeps variables, functions etc. names consistent across the entire codebase. Named exports have the benefit of\nerroring when import statements try to import something that hasn't been declared."}),"\n",(0,i.jsx)(n.h3,{id:"naming-conventions",children:"Naming Conventions"}),"\n",(0,i.jsx)(n.p,{children:"While it's often hard to find the best name, aim to optimize code for consistency and future reader by following conventions:"}),"\n",(0,i.jsx)(n.h4,{id:"variables-1",children:"Variables"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Locals"}),(0,i.jsx)(n.br,{}),"\n","Camel case",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"products"}),", ",(0,i.jsx)(n.code,{children:"productsFiltered"})]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Booleans"}),(0,i.jsx)(n.br,{}),"\n","Prefixed with ",(0,i.jsx)(n.code,{children:"is"}),", ",(0,i.jsx)(n.code,{children:"has"})," etc.",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"isDisabled"}),", ",(0,i.jsx)(n.code,{children:"hasProduct"})]}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/naming-convention",children:"'@typescript-eslint/naming-convention': [\n 'error',\n {\n selector: 'variable',\n types: ['boolean'],\n format: ['PascalCase'],\n prefix: ['is', 'are', 'should', 'has', 'can', 'did', 'will'],\n }\n]\n"}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Constants"}),(0,i.jsx)(n.br,{}),"\n","Capitalized",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"PRODUCT_ID"})]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Object constants"})}),"\n",(0,i.jsx)(n.p,{children:"Singular, capitalized with const assertion."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"const ORDER_STATUS = {\n pending: 'pending',\n fulfilled: true,\n error: 'Shipping Error',\n} as const;\n"})}),"\n",(0,i.jsxs)(n.p,{children:["If type exist use ",(0,i.jsx)(n.code,{children:"satisfies"})," operator in conjunction with const assertion, to conform object matches its type."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// OrderStatus is predefined (e.g. generated from database schema, API)\ntype OrderStatus = {\n pending: 'pending' | 'idle';\n fulfilled: boolean;\n error: string;\n};\n\nconst PENDING_STATUS = {\n pending: 'pending',\n fulfilled: true,\n error: 'Shipping Error',\n} as const satisfies OrderStatus;\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h4,{id:"functions-1",children:"Functions"}),"\n",(0,i.jsxs)(n.p,{children:["Camel case",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"filterProductsByType"}),", ",(0,i.jsx)(n.code,{children:"formatCurrency"})]}),"\n",(0,i.jsx)(n.h4,{id:"types-1",children:"Types"}),"\n",(0,i.jsxs)(n.p,{children:["Pascal case",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"OrderStatus"}),", ",(0,i.jsx)(n.code,{children:"ProductItem"})]}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/naming-convention",children:"\n'@typescript-eslint/naming-convention': [\n'error',\n{\n selector: 'typeAlias',\n format: ['PascalCase'],\n},\n]"}),"\n",(0,i.jsx)(n.h4,{id:"generics",children:"Generics"}),"\n",(0,i.jsxs)(n.p,{children:["A generic variable must start with the capital letter T followed by a descriptive name ",(0,i.jsx)(n.code,{children:"TRequest"}),", ",(0,i.jsx)(n.code,{children:"TFooBar"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["Creating more complex types often include generics, which can make them hard to read and understand, that's why we try to put best effort when naming them.",(0,i.jsx)(n.br,{}),"\n","Naming generics using popular convention with one letter ",(0,i.jsx)(n.code,{children:"T"}),", ",(0,i.jsx)(n.code,{children:"K"})," etc. is not allowed, the more variables we introduce, the easier it is to mistake them."]}),"\n",(0,i.jsx)(o.jO,{href:"https://typescript-eslint.io/rules/naming-convention",children:"'@typescript-eslint/naming-convention': [\n 'error',\n {\n // Generic type parameter must start with letter T, followed by any uppercase letter.\n selector: 'typeParameter',\n format: ['PascalCase'],\n custom: { regex: '^T[A-Z]', match: true },\n }\n]"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid naming generics with one letter\nconst createPair = (first: T, second: K): [T, K] => {\n return [first, second];\n};\nconst pair = createPair(1, 'a');\n\n// \u2705 Name starts with the capital letter T followed by a descriptive name\nconst createPair = (first: TFirst, second: TSecond): [TFirst, TSecond] => {\n return [first, second];\n};\nconst pair = createPair(1, 'a');\n"})}),"\n",(0,i.jsx)(n.h4,{id:"abbreviations--acronyms",children:"Abbreviations & Acronyms"}),"\n",(0,i.jsx)(n.p,{children:"Treat acronyms as whole words, with capitalized first letter only."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst FAQList = ['qa-1', 'qa-2'];\nconst generateUserURL(params) => {...}\n\n// \u2705 Use\nconst FaqList = ['qa-1', 'qa-2'];\nconst generateUserUrl(params) => {...}\n"})}),"\n",(0,i.jsx)(n.p,{children:"In favor of readability, strive to avoid abbreviations, unless they are widely accepted and necessary."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst GetWin(params) => {...}\n\n// \u2705 Use\nconst GetWindow(params) => {...}\n"})}),"\n",(0,i.jsx)(n.h4,{id:"react-components",children:"React Components"}),"\n",(0,i.jsxs)(n.p,{children:["Pascal case",(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"ProductItem"}),", ",(0,i.jsx)(n.code,{children:"ProductsPage"})]}),"\n",(0,i.jsx)(n.h4,{id:"prop-types",children:"Prop Types"}),"\n",(0,i.jsxs)(n.p,{children:['React component name following "Props" postfix',(0,i.jsx)(n.br,{}),"\n",(0,i.jsx)(n.code,{children:"[ComponentName]Props"})," - ",(0,i.jsx)(n.code,{children:"ProductItemProps"}),", ",(0,i.jsx)(n.code,{children:"ProductsPageProps"})]}),"\n",(0,i.jsx)(n.h4,{id:"callback-props",children:"Callback Props"}),"\n",(0,i.jsxs)(n.p,{children:["Event handler (callback) props are prefixed as ",(0,i.jsx)(n.code,{children:"on*"})," - e.g. ",(0,i.jsx)(n.code,{children:"onClick"}),".",(0,i.jsx)(n.br,{}),"\n","Event handler implementation functions are prefixed as ",(0,i.jsx)(n.code,{children:"handle*"})," - e.g. ",(0,i.jsx)(n.code,{children:"handleClick"}),"."]}),"\n",(0,i.jsx)(o.jO,{href:"https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-handler-names.md",children:"'react/jsx-handler-names': [\n 'error',\n {\n eventHandlerPrefix: 'handle',\n eventHandlerPropPrefix: 'on',\n },\n]"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-tsx",children:"// \u274c Avoid inconsistent callback prop naming\n\n\n\n// \u2705 Use prop prefix 'on*' and handler prefix 'handle*'\n\n\n"})}),"\n",(0,i.jsx)(n.h4,{id:"react-hooks",children:"React Hooks"}),"\n",(0,i.jsx)(o.jO,{prefix:"Camel case, prefixed as 'use'",href:"https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks",children:"'react-hooks/rules-of-hooks': 'error'"}),"\n",(0,i.jsx)(o.jO,{prefix:"Symmetrically convention as [value, setValue] = useState()",href:"https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/hook-use-state.md#rule-details",children:"'react/hook-use-state': 'error'"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid inconsistent useState hook naming\nconst [userName, setUser] = useState();\nconst [color, updateColor] = useState();\nconst [isActive, setActive] = useState();\n\n// \u2705 Use\nconst [name, setName] = useState();\nconst [color, setColor] = useState();\nconst [isActive, setIsActive] = useState();\n"})}),"\n",(0,i.jsx)(n.p,{children:"Custom hook must always return an object"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst [products, errors] = useGetProducts();\nconst [fontSizes] = useTheme();\n\n// \u2705 Use\nconst { products, errors } = useGetProducts();\nconst { fontSizes } = useTheme();\n"})}),"\n",(0,i.jsx)(n.h3,{id:"comments",children:"Comments"}),"\n",(0,i.jsx)(n.p,{children:"In general try to avoid comments, by writing expressive code and name things what they are."}),"\n",(0,i.jsxs)(n.p,{children:["Use comments when you need to add context or explain choices that cannot be expressed through code (e.g. config files).",(0,i.jsx)(n.br,{}),"\n","Comments should always be complete sentences. As rule of thumb try to explain ",(0,i.jsx)(n.code,{children:"why"})," in comments, not ",(0,i.jsx)(n.code,{children:"how"})," and ",(0,i.jsx)(n.code,{children:"what"}),"."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\n// convert to minutes\nconst m = s * 60;\n// avg users per minute\nconst myAvg = u / m;\n\n// \u2705 Use - Expressive code and name things what they are\nconst SECONDS_IN_MINUTE = 60;\nconst minutes = seconds * SECONDS_IN_MINUTE;\nconst averageUsersPerMinute = noOfUsers / minutes;\n\n// \u2705 Use - Add context to explain why in comments\n// TODO: Filtering should be moved to the backend once API changes are released.\n// Issue/PR - https://github.com/foo/repo/pulls/55124\nconst filteredUsers = frontendFiltering(selectedUsers);\n// Use Fourier transformation to minimize information loss - https://github.com/dntj/jsfft#usage\nconst frequencies = signal.FFT();\n"})}),"\n",(0,i.jsx)(n.h2,{id:"source-organization",children:"Source Organization"}),"\n",(0,i.jsx)(n.h3,{id:"code-collocation",children:"Code Collocation"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Every application or package in monorepo has project files/folders organized and grouped ",(0,i.jsx)(n.strong,{children:"by feature"}),"."]}),"\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.strong,{children:"Collocate code as close as possible to where it's relevant."})}),"\n",(0,i.jsx)(n.li,{children:"Deep folder nesting should not represent an issue."}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"imports",children:"Imports"}),"\n",(0,i.jsxs)(n.p,{children:["Import paths can be relative, starting with ",(0,i.jsx)(n.code,{children:"./"})," or ",(0,i.jsx)(n.code,{children:"../"}),", or they can be absolute ",(0,i.jsx)(n.code,{children:"@common/utils"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"To make import statements more readable and easier to understand:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Relative"})," imports ",(0,i.jsx)(n.code,{children:"./sortItems"})," must be used when importing files within the same feature, that are 'close' to each other, which also allows moving feature around the codebase without introducing changes in these imports."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Absolute"})," imports ",(0,i.jsx)(n.code,{children:"@common/utils"})," must be used in all other cases."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"All"})," imports must be auto sorted by tooling e.g. ",(0,i.jsx)(n.a,{href:"https://github.com/trivago/prettier-plugin-sort-imports",children:"prettier-plugin-sort-imports"}),", ",(0,i.jsx)(n.a,{href:"https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/order.md",children:"eslint-plugin-import"})," etc."]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nimport { bar, foo } from '../../../../../../distant-folder';\n\n// \u2705 Use\nimport { locationApi } from '@api/locationApi';\n\nimport { foo } from '../../foo';\nimport { bar } from '../bar';\nimport { baz } from './baz';\n"})}),"\n",(0,i.jsx)(n.h3,{id:"project-structure",children:"Project Structure"}),"\n",(0,i.jsx)(n.p,{children:"Example frontend monorepo project, where every application has file/folder grouped by feature:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"apps/\n\u251c\u2500 product-manager/\n\u2502 \u251c\u2500 common/\n\u2502 \u2502 \u251c\u2500 components/\n\u2502 \u2502 \u2502 \u251c\u2500 Button/\n\u2502 \u2502 \u2502 \u251c\u2500 ProductTitle/\n\u2502 \u2502 \u2502 \u251c\u2500 ...\n\u2502 \u2502 \u2502 \u2514\u2500 index.tsx\n\u2502 \u2502 \u251c\u2500 consts/\n\u2502 \u2502 \u2502 \u251c\u2500 paths.ts\n\u2502 \u2502 \u2502 \u2514\u2500 ...\n\u2502 \u2502 \u251c\u2500 hooks/\n\u2502 \u2502 \u2514\u2500 types/\n\u2502 \u251c\u2500 modules/\n\u2502 \u2502 \u251c\u2500 HomePage/\n\u2502 \u2502 \u251c\u2500 ProductAddPage/\n\u2502 \u2502 \u251c\u2500 ProductPage/\n\u2502 \u2502 \u251c\u2500 ProductsPage/\n\u2502 \u2502 \u2502 \u251c\u2500 api/\n\u2502 \u2502 \u2502 \u2502 \u2514\u2500 useGetProducts/\n\u2502 \u2502 \u2502 \u251c\u2500 components/\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500 ProductItem/\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500 ProductsStatistics/\n\u2502 \u2502 \u2502 \u2502 \u2514\u2500 ...\n\u2502 \u2502 \u2502 \u251c\u2500 utils/\n\u2502 \u2502 \u2502 \u2502 \u2514\u2500 filterProductsByType/\n\u2502 \u2502 \u2502 \u2514\u2500 index.tsx\n\u2502 \u2502 \u251c\u2500 ...\n\u2502 \u2502 \u2514\u2500 index.tsx\n\u2502 \u251c\u2500 eslintrc.js\n\u2502 \u251c\u2500 package.json\n\u2502 \u2514\u2500 tsconfig.json\n\u251c\u2500 warehouse/\n\u251c\u2500 admin-dashboard/\n\u2514\u2500 ...\n"})}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"modules"})," folder is responsible for implementation of each individual page, where all custom features for that page are being implemented (components, hooks, utils functions etc.)."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"common"}),' folder is responsible for implementations that are truly used across application. Since it\'s a "global folder" it should be used sparingly.',(0,i.jsx)(n.br,{}),"\n","If same component e.g. ",(0,i.jsx)(n.code,{children:"common/components/ProductTitle"})," starts being used on more than one page, it shall be moved to common folder."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["In case using frontend framework with file-system based router (e.g. Nextjs), ",(0,i.jsx)(n.code,{children:"pages"})," folder serves only as a router, where its responsibility is to define routes (no business logic implementation)."]}),"\n",(0,i.jsx)(n.p,{children:"Example backend project structure with file/folder grouped by feature:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"product-manager/\n\u251c\u2500 dist/\n\u251c\u2500\u2500 database/\n\u2502 \u251c\u2500\u2500 migrations/\n\u2502 \u2502 \u251c\u2500\u2500 20220102063048_create_accounts.ts\n\u2502 \u2502 \u2514\u2500\u2500 ...\n\u2502 \u2514\u2500\u2500 seeders/\n\u2502 \u251c\u2500\u2500 20221116042655-feeds.ts\n\u2502 \u2514\u2500\u2500 ...\n\u251c\u2500 docker/\n\u251c\u2500 logs/\n\u251c\u2500 scripts/\n\u251c\u2500 src/\n\u2502 \u251c\u2500 common/\n\u2502 \u2502 \u251c\u2500 consts/\n\u2502 \u2502 \u251c\u2500 middleware/\n\u2502 \u2502 \u251c\u2500 types/\n\u2502 \u2502 \u2514\u2500 ...\n\u2502 \u251c\u2500 modules/\n\u2502 \u2502 \u251c\u2500\u2500 admin/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 account/\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.model.ts\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.controller.ts\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.route.ts\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.service.ts\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.validation.ts\n\u2502 \u2502 \u2502 \u2502 \u251c\u2500\u2500 account.test.ts\n\u2502 \u2502 \u2502 \u2502 \u2514\u2500\u2500 index.ts\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 ...\n\u2502 \u2502 \u251c\u2500\u2500 general/\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.model.ts\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.controller.ts\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.route.ts\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.service.ts\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.validation.ts\n\u2502 \u2502 \u2502 \u251c\u2500\u2500 general.test.ts\n\u2502 \u2502 \u2502 \u2514\u2500\u2500 index.ts\n\u2502 \u2502 \u251c\u2500 ...\n\u2502 \u2502 \u2514\u2500 index.tsx\n\u2502 \u2514\u2500 ...\n\u251c\u2500 ...\n\u251c\u2500 eslintrc.js\n\u251c\u2500 package.json\n\u2514\u2500 tsconfig.json\n"})}),"\n",(0,i.jsx)(n.h2,{id:"appendix---react",children:"Appendix - React"}),"\n",(0,i.jsxs)(n.p,{children:["Since React components and hooks are also functions, respective ",(0,i.jsx)(n.a,{href:"#functions",children:"function conventions"})," applies."]}),"\n",(0,i.jsx)(n.h3,{id:"required--optional-props",children:"Required & Optional Props"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Strive to have majority of props required and use optional sparingly."})}),"\n",(0,i.jsxs)(n.p,{children:["Especially when creating new component for first/single use case majority of props should be required. When component starts covering more use cases, introduce optional props.",(0,i.jsx)(n.br,{}),"\n","There are potential exceptions, where component API needs to implement optional props from the start (e.g. shared components covering multiple use cases, UI design system components - button ",(0,i.jsx)(n.code,{children:"isDisabled"})," etc.)"]}),"\n",(0,i.jsxs)(n.p,{children:["If component/hook becomes to complex it probably should be broken into smaller pieces.",(0,i.jsx)(n.br,{}),"\n",'An exaggerated example where implementing 10 React components with 5 required props each, is better then implementing one "can do it all" component that accepts 50 optional props.']}),"\n",(0,i.jsx)(n.h3,{id:"props-as-discriminated-type",children:"Props as Discriminated Type"}),"\n",(0,i.jsxs)(n.p,{children:["When applicable use ",(0,i.jsx)(n.strong,{children:"discriminated type"})," to eliminate optional props, which will decrease complexity on component API and only required props will be passed depending on its use case."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid optional props as they increase complexity of component API\ntype StatusProps = {\n data?: Products;\n title?: string;\n time?: number;\n error?: string;\n};\n\n// \u2705 Strive to have majority of props required, if that's not possible,\n// use discriminated union for clear intent on component usage\ntype StatusSuccess = {\n status: 'success';\n data: Products;\n title: string;\n};\n\ntype StatusLoading = {\n status: 'loading';\n time: number;\n};\n\ntype StatusError = {\n status: 'error';\n error: string;\n};\n\n// Discriminated component props 'StatusProps' with no optional properties\ntype StatusProps = StatusSuccess | StatusLoading | StatusError;\n\nexport const Status = (status: StatusProps) => {...\n"})}),"\n",(0,i.jsx)(n.h3,{id:"props-to-state",children:"Props To State"}),"\n",(0,i.jsxs)(n.p,{children:["In general avoid using props to state, since component will not update on prop changes. It can lead to bugs that are hard to track, with unintended side effects and difficulty testing.",(0,i.jsx)(n.br,{}),"\n","When there is truly a use case for using prop in initial state, prop must be prefixed with ",(0,i.jsx)(n.code,{children:"initial"})," (e.g. ",(0,i.jsx)(n.code,{children:"initialProduct"}),", ",(0,i.jsx)(n.code,{children:"initialSort"})," etc.)"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-tsx",children:"// \u274c Avoid using props to state\ntype FooProps = {\n productName: string;\n userId: string;\n};\n\nexport const Foo = ({ productName, userId }: FooProps) => {\n const [productName, setProductName] = useState(productName);\n ...\n\n// \u2705 Use prop prefix `initial`, when there is a rational use case for it\ntype FooProps = {\n initialProductName: string;\n userId: string;\n};\n\nexport const Foo = ({ initialProductName, userId }: FooProps) => {\n const [productName, setProductName] = useState(initialProductName);\n ...\n"})}),"\n",(0,i.jsx)(n.h3,{id:"props-type",children:"Props Type"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-tsx",children:"// \u274c Avoid using React.FC type\ntype FooProps = {\n name: string;\n score: number;\n};\n\nexport const Foo: React.FC = ({ name, score }) => {\n\n// \u2705 Use props argument with type\ntype FooProps = {\n name: string;\n score: number;\n};\n\nexport const Foo = ({ name, score }: FooProps) => {...\n"})}),"\n",(0,i.jsx)(n.h3,{id:"component-types",children:"Component Types"}),"\n",(0,i.jsx)(n.h4,{id:"container",children:"Container"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:['All container components have postfix "Container" or "Page" ',(0,i.jsx)(n.code,{children:"[ComponentName]Container|Page"}),'. Use "Page" postfix to indicate component is an actual web page.']}),"\n",(0,i.jsxs)(n.li,{children:["Each feature has a container component (",(0,i.jsx)(n.code,{children:"AddUserContainer.tsx"}),", ",(0,i.jsx)(n.code,{children:"EditProductContainer.tsx"}),", ",(0,i.jsx)(n.code,{children:"ProductsPage.tsx"})," etc.)"]}),"\n",(0,i.jsx)(n.li,{children:"Includes business logic."}),"\n",(0,i.jsx)(n.li,{children:"API integration."}),"\n",(0,i.jsxs)(n.li,{children:["Structure:","\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"ProductsPage/\n\u251c\u2500 api/\n\u2502 \u2514\u2500 useGetProducts/\n\u251c\u2500 components/\n\u2502 \u2514\u2500 ProductItem/\n\u251c\u2500 utils/\n\u2502 \u2514\u2500 filterProductsByType/\n\u2514\u2500 index.tsx\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h4,{id:"ui---feature",children:"UI - Feature"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Representational components that are designed to fulfill feature requirements."}),"\n",(0,i.jsx)(n.li,{children:"Nested inside container component folder."}),"\n",(0,i.jsxs)(n.li,{children:["Should follow ",(0,i.jsx)(n.a,{href:"#functions",children:"functions conventions"})," as much as possible."]}),"\n",(0,i.jsx)(n.li,{children:"No API integration."}),"\n",(0,i.jsxs)(n.li,{children:["Structure:","\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"ProductItem/\n\u251c\u2500 index.tsx\n\u251c\u2500 ProductItem.stories.tsx\n\u2514\u2500 ProductItem.test.tsx\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h4,{id:"ui---design-system",children:"UI - Design system"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Global Reusable/shared components used throughout whole codebase."}),"\n",(0,i.jsxs)(n.li,{children:["Structure:","\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"Button/\n\u251c\u2500 index.tsx\n\u251c\u2500 Button.stories.tsx\n\u2514\u2500 Button.test.tsx\n"})}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"store--pass-data",children:"Store & Pass Data"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Pass only the necessary props to child components rather than passing the entire object."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Utilize storing state in the URL, especially for filtering, sorting etc."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Don't sync URL state with local state."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Consider passing data simply through props, using the URL, or composing children. Use global state (Zustand, Context) as a last resort."}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsxs)(n.p,{children:["Use React compound components when components should belong and work together: ",(0,i.jsx)(n.code,{children:"menu"}),", ",(0,i.jsx)(n.code,{children:"accordion"}),",",(0,i.jsx)(n.code,{children:"navigation"}),", ",(0,i.jsx)(n.code,{children:"tabs"}),", ",(0,i.jsx)(n.code,{children:"list"}),", etc.",(0,i.jsx)(n.br,{}),"\n","Always export compound components as:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-tsx",children:'// PriceList.tsx\nconst PriceListRoot = ({ children }) =>
// ❌ Avoid @ts-ignore as it will do nothing if the following line is error-free. // @ts-ignore const newUser =createUser('Gabriel'); // ✅ Use @ts-expect-error with description. // @ts-expect-error: This library function has incorrect type definitions - createUser accepts string as an argument. const newUser =createUser('Gabriel');
modules folder is responsible for implementation of each individual page, where all custom features for that page are being implemented (components, hooks, utils functions etc.).
common folder is responsible for implementations that are truly used across application. Since it's a "global folder" it should be used sparingly.
diff --git a/search.html b/search.html
index a9bf439..935f584 100644
--- a/search.html
+++ b/search.html
@@ -4,7 +4,7 @@
Search the documentation |
-
+