From 3292eecc6ab33ebb378cce4dce6cc5e98496b760 Mon Sep 17 00:00:00 2001 From: mkosir Date: Thu, 9 Jan 2025 10:13:42 +0000 Subject: [PATCH] =?UTF-8?q?Deploying=20to=20gh-pages=20from=20@=20mkosir/t?= =?UTF-8?q?ypescript-style-guide@c7f8a0605cbb6eeeda37d7eb78af25fd2ecc8d76?= =?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.54de0a24.js => e7ce6630.ce0090df.js} | 2 +- .../js/{runtime~main.4bbe0b7d.js => runtime~main.2b66c796.js} | 2 +- index.html | 4 ++-- search.html | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename assets/js/{e7ce6630.54de0a24.js => e7ce6630.ce0090df.js} (54%) rename assets/js/{runtime~main.4bbe0b7d.js => runtime~main.2b66c796.js} (77%) diff --git a/404.html b/404.html index 7ba2e68..22a66cd 100644 --- a/404.html +++ b/404.html @@ -4,7 +4,7 @@ Page Not Found | - + diff --git a/assets/js/e7ce6630.54de0a24.js b/assets/js/e7ce6630.ce0090df.js similarity index 54% rename from assets/js/e7ce6630.54de0a24.js rename to assets/js/e7ce6630.ce0090df.js index 8e74f98..bc87e3c 100644 --- a/assets/js/e7ce6630.54de0a24.js +++ b/assets/js/e7ce6630.ce0090df.js @@ -1 +1 @@ -"use strict";(self.webpackChunktypescript_style_guide_website=self.webpackChunktypescript_style_guide_website||[]).push([[490],{7391:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>u,contentTitle:()=>h,default:()=>g,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 for creating consistent, maintainable code.","frontMatter":{"title":"TypeScript Style Guide","description":"TypeScript Style Guide provides a concise set of conventions and best practices for creating consistent, maintainable code.","toc_min_heading_level":2,"toc_max_heading_level":2},"unlisted":false}');var i=s(4848),r=s(8453),o=s(7712),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 for creating 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:"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:"Return Types",id:"return-types",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 for creating 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.jsx)(n.p,{children:'Since "consistency is the key", TypeScript Style Guide strives to enforce the majority of rules using automated tools such as ESLint, TypeScript, Prettier, etc.\nHowever, certain design and architectural decisions must still be followed, as described in the 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 approach to writing TypeScript applications leads to a consistent codebase and faster development cycles."}),"\n",(0,i.jsx)(n.li,{children:"No need to discuss code styles during 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:"Like any code style guide, this one is opinionated, setting conventions (sometimes arbitrary) to govern our code."}),"\n",(0,i.jsx)(n.p,{children:"You don't have to follow every convention exactly as written, decide what works best for your product and team to maintain consistency in your codebase."}),"\n",(0,i.jsx)(n.h3,{id:"requirements",children:"Requirements"}),"\n",(0,i.jsx)(n.p,{children:"This Style Guide requires the use of:"}),"\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 v8"})," 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:"The Style Guide assumes the use of, 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, consider 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"})," offers several 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, as narrowed types provide more specific information about the shape and behavior of your data."}),"\n",(0,i.jsx)(n.li,{children:"Improved Code Clarity - Reduces cognitive load by providing clearer boundaries and constraints on your data, making your code easier for other developers to understand."}),"\n",(0,i.jsx)(n.li,{children:"Easier Refactoring - With narrower types, making changes to your code becomes less risky, allowing you to refactor with confidence."}),"\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 a rule of thumb, explicitly declare types only when it helps to narrow them."}),"\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, explicitly declaring types can\nimprove code readability and clarify intent."})}),"\n",(0,i.jsx)(n.p,{children:"Explicitly declare types when doing so helps to narrow them:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst employees = new Map(); // Inferred as wide type 'Map'\nemployees.set('Lea', 17);\ntype UserRole = 'admin' | 'guest';\nconst [userRole, setUserRole] = useState('admin'); // Inferred as 'string', not the desired narrowed literal type\n\n// \u2705 Use explicit type declarations to narrow the types.\nconst employees = new Map(); // Narrowed to 'Map'\nemployees.set('Gabriel', 32);\ntype UserRole = 'admin' | 'guest';\nconst [userRole, setUserRole] = useState('admin'); // Explicit type 'UserRole'\n"})}),"\n",(0,i.jsx)(n.p,{children:"Avoid explicitly declaring types when they can be inferred:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst userRole: string = 'admin'; // Inferred as wide type 'string'\nconst employees = new Map([['Gabriel', 32]]); // Redundant type declaration\nconst [isActive, setIsActive] = useState(false); // Redundant, inferred as 'boolean'\n\n// \u2705 Use type inference.\nconst USER_ROLE = 'admin'; // Inferred as narrowed string literal type 'admin'\nconst employees = new Map([['Gabriel', 32]]); // Inferred as 'Map'\nconst [isActive, setIsActive] = useState(false); // Inferred as 'boolean'\n"})}),"\n",(0,i.jsx)(n.h3,{id:"data-immutability",children:"Data Immutability"}),"\n",(0,i.jsxs)(n.p,{children:["Immutability should be a key principle. Wherever possible, data should remain immutable, leveraging types like ",(0,i.jsx)(n.code,{children:"Readonly"})," and ",(0,i.jsx)(n.code,{children:"ReadonlyArray"}),"."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Using readonly types prevents accidental data mutations and reduces the risk of bugs caused by unintended side effects. This ensures that data integrity is maintained throughout the application lifecycle."}),"\n",(0,i.jsx)(n.li,{children:"When performing data processing, always return new arrays, objects, or other reference-based data structures. To minimize cognitive load for future developers, strive to keep data objects flat and concise."}),"\n",(0,i.jsx)(n.li,{children:"Mutations should be used sparingly, in cases where they are truly necessary, such as when dealing with complex objects or optimizing for performance."}),"\n"]}),"\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 when possible, as they increase complexity and ambiguity\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 Prefer required properties. If optional properties are unavoidable,\n// use a discriminated union to make object usage explicit and predictable.\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' ensures clear intent 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:"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.p,{children:"Array constants:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"type 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",(0,i.jsx)(n.p,{children:"Object constants:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"type 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.jsx)(n.p,{children:"Embrace template literal types as they allow you to create precise and type-safe string constructs by interpolating values. They are a powerful alternative to using the wide string type, providing better type safety."}),"\n",(0,i.jsx)(n.p,{children:"Adopting template literal types brings several advantages:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Prevent errors caused by typos or invalid strings."}),"\n",(0,i.jsx)(n.li,{children:"Provide better type safety and autocompletion support."}),"\n",(0,i.jsx)(n.li,{children:"Improve code maintainability and readability."}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Template literal types are useful in various practical scenarios, such as:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"API Endpoints - Use template literal types to restrict values to valid API routes."}),"\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"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Internationalization Keys - Avoid relying on raw strings for translation keys, which can lead to typos and missing translations. Use template literal types to define valid translation keys."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \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"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"CSS Utilities - Avoid raw strings for color values, which can lead to invalid or non-existent colors. Use template literal types to enforce valid color names and values."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \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"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Database queries - Avoid using raw strings for table or column names, which can lead to typos and invalid queries. Use template literal types to define valid tables and column combinations."}),"\n"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst query = 'SELECT name FROM usersss WHERE age > 30'; // Type 'string' - Typo 'usersss': table doesn't exist, leading to a runtime error.\n// \u2705 Use\ntype Table = 'users' | 'posts' | 'comments';\ntype Column =\n TTableName extends 'users' ? 'id' | 'name' | 'age' :\n TTableName extends 'posts' ? 'id' | 'title' | 'content' :\n TTableName extends 'comments' ? 'id' | 'postId' | 'text' :\n never;\n\ntype Query = `SELECT ${Column} FROM ${TTableName} WHERE ${string}`;\nconst userQuery: Query<'users'> = 'SELECT name FROM users WHERE age > 30'; // Valid query\nconst invalidQuery: Query<'users'> = 'SELECT title FROM users WHERE age > 30'; // Error: 'title' is not a column in 'users' table.\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 tracking of suppressed errors."]}),"\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 provides two options for defining types: ",(0,i.jsx)(n.code,{children:"type"})," and ",(0,i.jsx)(n.code,{children:"interface"}),". While these options have some functional differences, they are interchangeable in most cases. To maintain consistency, choose one and use it consistently."]}),"\n",(0,i.jsx)(o.jO,{prefix:"Define all types using type alias",href:"https://typescript-eslint.io/rules/consistent-type-definitions",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 might be extended by third-party consumers in the future or\nwhen your team prefers working with interfaces. In these cases, you can disable linting rules if needed, such as when\ndefining 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 - interfaces can't define 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 and ambiguity in function APIs\ntype StatusParams = {\n data?: Products;\n title?: string;\n time?: number;\n error?: string;\n};\n\n// \u2705 Prefer required properties. If optional properties are unavoidable,\n// use a discriminated union to represent distinct use cases with required properties.\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 union 'StatusParams' ensures predictable function arguments with no optional properties\ntype StatusParams = StatusSuccessParams | StatusLoadingParams | StatusErrorParams;\n\nexport const parseStatus = (params: StatusParams) => {...\n"})}),"\n",(0,i.jsx)(n.h3,{id:"return-types",children:"Return Types"}),"\n",(0,i.jsx)(o.jO,{prefix:"Explicitly defining the return type of a function is 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 the advantages of explicitly defining the return type of a function::"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Improves Readability"}),": Clearly specifies what type of value the function returns, making the code easier to understand for those calling the function."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Avoids Misuse"}),": Ensures that calling code does not accidentally attempt to use an undefined value when no return value is intended."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Surfaces Type Errors Early"}),": Helps catch potential type errors during development, especially when code changes unintentionally alter the return type."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Simplifies Refactoring"}),": Ensures that any variable assigned to the function's return value is of the correct type, making refactoring safer and more efficient."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Encourages Design Discussions"}),": Similar to Test-Driven Development (TDD), explicitly defining function arguments and return types promotes discussions about a function's functionality and interface ahead of implementation."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Optimizes Compilation"}),": While TypeScript's type inference is powerful, explicitly defining return types can reduce the workload on the TypeScript compiler, improving overall performance."]}),"\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 declaring constants using const assertion ",(0,i.jsx)(n.code,{children:"as const"}),":"]}),"\n",(0,i.jsx)(n.p,{children:"Constants are used to represent values that are not meant to change, ensuring reliability and consistency in a codebase. Using const assertions further enhances type safety and immutability, making your code more robust and predictable."}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["Type Narrowing - Using ",(0,i.jsx)(n.code,{children:"as const"})," ensures that literal values (e.g., numbers, strings) are treated as exact values instead of generalized types like ",(0,i.jsx)(n.code,{children:"number"})," or ",(0,i.jsx)(n.code,{children:"string"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Immutability - Objects and arrays get readonly properties, preventing accidental mutations."}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Examples:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Objects"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst FOO_LOCATION = { x: 50, y: 130 }; // Type { x: number; y: number; }\nFOO_LOCATION.x = 10;\n\n// \u2705 Use\nconst FOO_LOCATION = { x: 50, y: 130 } as const; // Type '{ readonly x: 50; readonly y: 130; }'\nFOO_LOCATION.x = 10; // Error\n"})}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Arrays"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst BAR_LOCATION = [50, 130]; // Type number[]\nBAR_LOCATION.push(10);\n\n// \u2705 Use\nconst BAR_LOCATION = [50, 130] as const; // Type 'readonly [10, 20]'\nBAR_LOCATION.push(10); // Error\n"})}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Template Literals"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst RATE_LIMIT = 25;\nconst RATE_LIMIT_MESSAGE = `Max number of requests/min is ${RATE_LIMIT}.`; // Type string\n\n// \u2705 Use\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 assertions must be used over enums."}),"\n",(0,i.jsxs)(n.p,{children:["While enums can still fulfill similar use cases as const assertions, we tend to avoid using them.",(0,i.jsx)(n.br,{}),"\n","The TypeScript documentation highlights several reasons into the limitations and potential issues with enums, such as:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls",children:"Const enum pitfalls"})}),"\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums",children:"Objects vs Enums"})}),"\n",(0,i.jsx)(n.li,{children:(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings",children:"Reverse mappings"})}),"\n"]}),"\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: 'Replace enum with a literal type or a const assertion.',\n },\n]"}),"\n",(0,i.jsx)(n.p,{children:"As rule of a thumb, try to:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Use literal types whenever possible."}),"\n",(0,i.jsx)(n.li,{children:"Use const assertion arrays when you need to loop through the values."}),"\n",(0,i.jsx)(n.li,{children:"Use const assertion objects when there is a strong use case for them."}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Examples:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Use literal types whenever possible to avoid creating runtime objects that add to the bundle size."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid using enums as they increase the bundle size\nenum UserRole {\n GUEST = 'guest',\n MODERATOR = 'moderator',\n ADMINISTRATOR = 'administrator',\n}\n\n// Transpiled JavaScript\n('use strict');\nvar UserRole;\n(function (UserRole) {\n UserRole['GUEST'] = 'guest';\n UserRole['MODERATOR'] = 'moderator';\n UserRole['ADMINISTRATOR'] = 'administrator';\n})(UserRole || (UserRole = {}));\n\n// \u2705 Use literal types\ntype UserRole = 'guest' | 'moderator' | 'administrator';\n\nconst isGuest = (role: UserRole) => role === 'guest';\n"})}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Use const assertion arrays when you need to loop through the values."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-tsx",children:"// \u274c Avoid using enums\nenum UserRole {\n GUEST = 'guest',\n MODERATOR = 'moderator',\n ADMINISTRATOR = 'administrator',\n}\n\n// \u2705 Use const assertions arrays\nconst USER_ROLES = ['guest', 'moderator', 'administrator'] as const;\ntype UserRole = (typeof USER_ROLES)[number];\n\nconst seedDatabase = () => {\n USER_ROLES.forEach((role) => {\n db.roles.insert(role);\n }\n}\nconst insert = (role: UserRole) => {...\n\nconst UsersRoleList = () => {\n return (\n
\n {USER_ROLES.map((role) => (\n \n ))}\n
\n );\n};\nconst Item = ({ role }: { role: UserRole }) => {...\n"})}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["\n",(0,i.jsx)(n.p,{children:"Use const assertion objects when there is a strong use case for them."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid using enums\nenum Color {\n PRIMARY = '#B33930',\n SECONDARY = '#113A5C',\n BRAND = '#9C0E7D',\n}\n\n// \u2705 Use const assertions objects\nconst COLOR = {\n primary: '#B33930',\n secondary: '#113A5C',\n brand: '#9C0E7D',\n} as const;\n\ntype Color = typeof COLOR;\ntype ColorKey = keyof Color; // Type \"primary\" | \"secondary\" | \"brand\"\ntype ColorValue = Color[ColorKey]; // Type \"#B33930\" | \"#113A5C\" | \"#9C0E7D\"\n\nconst setColor = (color: ColorValue) => {...\n\nsetColor(COLOR.primary);\nsetColor('#B33930');\n"})}),"\n"]}),"\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 {\n files: ["src/pages/**/*"],\n rules: { "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 +
// ❌ Avoid using enums as they increase the bundle size
enum UserRole {
GUEST = 'guest',
MODERATOR = 'moderator',
ADMINISTRATOR = 'administrator',
}

// Transpiled JavaScript
('use strict');
var UserRole;
(function (UserRole) {
UserRole['GUEST'] = 'guest';
UserRole['MODERATOR'] = 'moderator';
UserRole['ADMINISTRATOR'] = 'administrator';
})(UserRole || (UserRole = {}));

// ✅ Use literal types - Types are stripped during transpilation
type UserRole = 'guest' | 'moderator' | 'administrator';

const isGuest = (role: UserRole) => role === 'guest';
  • Use const assertion arrays when you need to loop through the values.

    diff --git a/search.html b/search.html index 73a8f61..de84853 100644 --- a/search.html +++ b/search.html @@ -4,7 +4,7 @@ Search the documentation | - +