diff --git a/404.html b/404.html index da2c1126..4acc723f 100644 --- a/404.html +++ b/404.html @@ -4,7 +4,7 @@ Page Not Found | - + diff --git a/assets/js/e7ce6630.74b177d6.js b/assets/js/e7ce6630.74b177d6.js deleted file mode 100644 index 83dae81f..00000000 --- a/assets/js/e7ce6630.74b177d6.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunktypescript_style_guide_website=self.webpackChunktypescript_style_guide_website||[]).push([[490],{4628:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>u,contentTitle:()=>p,default:()=>g,frontMatter:()=>d,metadata:()=>h,toc:()=>m});var t=s(4848),i=s(8453),r=s(5906),o=s(6540);const a=e=>{const n=e.substring(1).replace(/---/g,"__dash__").replace(/--/g," & ").replace(/-/g," ").replace(/__dash__/g," - ");return l(n)},l=e=>e.toLowerCase().split(" ").map((e=>e[0]?.toUpperCase()+e.substring(1))).join(" "),c=e=>{let{children:n}=e;var s;return s=n,(0,o.useEffect)((()=>{const e=()=>{const e=window.location.hash;document.title=e?`${a(e)} | ${s}`:s};return e(),window.addEventListener("popstate",e),()=>{window.removeEventListener("popstate",e)}}),[s]),null},d={title:"TypeScript Style Guide",description:"TypeScript Style Guide provides a concise set of conventions and best practices used to create consistent, maintainable code.",toc_min_heading_level:2,toc_max_heading_level:2},p=void 0,h={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 used to create consistent, maintainable code.",frontMatter:{title:"TypeScript Style Guide",description:"TypeScript Style Guide provides a concise set of conventions and best practices used to create consistent, maintainable code.",toc_min_heading_level:2,toc_max_heading_level:2},unlisted:!1},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:"Data Immutability",id:"data-immutability",level:2},{value:"Types",id:"types",level:2},{value:"Type Inference",id:"type-inference",level:3},{value:"Return Types",id:"return-types",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:"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 Union",id:"args-as-discriminated-union",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:"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:"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,i.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(c,{children:"TypeScript Style Guide"}),"\n",(0,t.jsx)(r.OB,{children:"TypeScript Style Guide"}),"\n",(0,t.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,t.jsx)(n.p,{children:"TypeScript Style Guide provides a concise set of conventions and best practices used to create consistent, maintainable code."}),"\n",(0,t.jsx)(n.h2,{id:"table-of-contents",children:"Table of Contents"}),"\n",(0,t.jsx)(r.MB,{items:m}),"\n",(0,t.jsx)(n.h2,{id:"about-guide",children:"About Guide"}),"\n",(0,t.jsx)(n.h3,{id:"what",children:"What"}),"\n",(0,t.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,t.jsx)(n.br,{}),"\n","Still certain design and architectural decisions must be followed which are described with conventions below."]}),"\n",(0,t.jsx)(n.h3,{id:"why",children:"Why"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"As project grow in size and complexity, maintaining code quality and ensuring consistent practices becomes increasingly challenging."}),"\n",(0,t.jsx)(n.li,{children:"Defining and following a standard way to write TypeScript applications brings consistent codebase and faster development cycles."}),"\n",(0,t.jsx)(n.li,{children:"No need to discuss code styles in code reviews."}),"\n",(0,t.jsx)(n.li,{children:"Saves team time and energy."}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"disclaimer",children:"Disclaimer"}),"\n",(0,t.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,t.jsx)(n.p,{children:"You don't have to follow every convention exactly as it is written in guide, decide what works best for your product and team to stay consistent with your codebase."}),"\n",(0,t.jsx)(n.h3,{id:"requirements",children:"Requirements"}),"\n",(0,t.jsx)(n.p,{children:"Style Guide requires you to use:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:(0,t.jsx)(n.a,{href:"https://github.com/microsoft/TypeScript",children:"TypeScript v5"})}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.a,{href:"https://github.com/typescript-eslint/typescript-eslint",children:"typescript-eslint v7"})," with ",(0,t.jsx)(n.a,{href:"https://typescript-eslint.io/linting/configs/#strict-type-checked",children:(0,t.jsx)(n.code,{children:"strict-type-checked"})})," configuration enabled."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Style Guide assumes using, but is not limited to:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.a,{href:"https://github.com/facebook/react",children:"React"})," as UI library for frontend conventions."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.a,{href:"https://playwright.dev/",children:"Playwright"})," and ",(0,t.jsx)(n.a,{href:"https://testing-library.com/",children:"Testing Library"})," for testing conventions."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"tldr",children:"TLDR"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Strive for data immutability. ",(0,t.jsx)(n.a,{href:"#data-immutability",children:"\u2b63"})]}),"\n",(0,t.jsxs)(n.li,{children:["Embrace const assertions. ",(0,t.jsx)(n.a,{href:"#const-assertion",children:"\u2b63"})]}),"\n",(0,t.jsxs)(n.li,{children:["Avoid type assertions. ",(0,t.jsx)(n.a,{href:"#type--non-nullability-assertions",children:"\u2b63"})]}),"\n",(0,t.jsxs)(n.li,{children:["Strive for functions to be pure, stateless and have single responsibility. ",(0,t.jsx)(n.a,{href:"#functions",children:"\u2b63"})]}),"\n",(0,t.jsxs)(n.li,{children:["Majority of function arguments should be required (use optional sparingly). ",(0,t.jsx)(n.a,{href:"#required--optional-args",children:"\u2b63"})]}),"\n",(0,t.jsxs)(n.li,{children:["Strong emphasis to keep naming conventions consistent and readable. ",(0,t.jsx)(n.a,{href:"#naming-conventions",children:"\u2b63"})]}),"\n",(0,t.jsxs)(n.li,{children:["Use named exports. ",(0,t.jsx)(n.a,{href:"#named-export",children:"\u2b63"})]}),"\n",(0,t.jsxs)(n.li,{children:["Code is organized and grouped by feature. Collocate code as close as possible to where it's relevant. ",(0,t.jsx)(n.a,{href:"#code-collocation",children:"\u2b63"})]}),"\n",(0,t.jsxs)(n.li,{children:["UI components must only show derived state and send events, nothing more (no business logic). ",(0,t.jsx)(n.a,{href:"#component-types",children:"\u2b63"})]}),"\n",(0,t.jsxs)(n.li,{children:["Test business logic, not implementation details. ",(0,t.jsx)(n.a,{href:"#what--how-to-test",children:"\u2b63"})]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"data-immutability",children:"Data Immutability"}),"\n",(0,t.jsxs)(n.p,{children:["Majority of the data should be immutable (use ",(0,t.jsx)(n.code,{children:"Readonly"}),", ",(0,t.jsx)(n.code,{children:"ReadonlyArray"}),", always return new array, object etc). To keep cognitive load for future developers low, try to keep data objects small.",(0,t.jsx)(n.br,{}),"\n","As an exception mutations should be used sparingly in cases where truly necessary: complex objects, performance reasoning etc."]}),"\n",(0,t.jsx)(n.h2,{id:"types",children:"Types"}),"\n",(0,t.jsx)(n.h3,{id:"type-inference",children:"Type Inference"}),"\n",(0,t.jsx)(n.p,{children:"As rule of thumb, explicitly declare a type when it help narrows it."}),"\n",(0,t.jsx)(r.vu,{children:(0,t.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,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid - Type 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\n// \u274c Avoid - Type can be narrowed\nconst employees = new Map(); // Type 'Map'\nemployees.set('Lea', 'foo-anything');\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,t.jsx)(n.h3,{id:"return-types",children:"Return Types"}),"\n",(0,t.jsxs)(n.p,{children:["Including return type annotations is highly encouraged, although not required (",(0,t.jsx)(n.a,{href:"https://typescript-eslint.io/rules/explicit-function-return-type/",children:"eslint rule"}),")."]}),"\n",(0,t.jsx)(n.p,{children:"Consider benefits when explicitly typing the return value of a function:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Return values makes it clear and easy to understand to any calling code what type is returned."}),"\n",(0,t.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,t.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,t.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,t.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,t.jsx)(n.li,{children:"Although type inference is very convenient, adding return types can save TypeScript compiler a lot of work."}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"template-literal-types",children:"Template Literal Types"}),"\n",(0,t.jsxs)(n.p,{children:["Embrace using template literal types, instead of just (wide) ",(0,t.jsx)(n.code,{children:"string"})," type.",(0,t.jsx)(n.br,{}),"\n","Template literal types have many applicable use cases e.g. API endpoints, routing, internationalization, database queries, CSS typings ..."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst userEndpoint = '/api/users'; // Type 'string' - no error\n// \u2705 Use\ntype ApiRoute = 'users' | 'posts' | 'comments';\ntype ApiEndpoint = `/api/${ApiRoute}`;\nconst userEndpoint: ApiEndpoint = '/api/users';\n\n// \u274c Avoid\nconst homeTitleTranslation = 'translation.home.title'; // Type 'string' - no error\n// \u2705 Use\ntype LocaleKeyPages = 'home' | 'about' | 'contact';\ntype TranslationKey = `translation.${LocaleKeyPages}.${string}`;\nconst homeTitleTranslation: TranslationKey = 'translation.home.title';\n\n// \u274c Avoid\nconst color = 'blue-450'; // Type 'string' - no error\n// \u2705 Use\ntype BaseColor = 'blue' | 'red' | 'yellow' | 'gray';\ntype Variant = 50 | 100 | 200 | 300 | 400;\n// Type blue-50, blue-100, blue-200, blue-300, blue-400, red-50, red-100, #AD3128 ...\ntype Color = `${BaseColor}-${Variant}` | `#${string}`;\n"})}),"\n",(0,t.jsx)(n.h3,{id:"type-any--unknown",children:"Type any & unknown"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.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,t.jsx)(n.code,{children:"any"})," is dangerous, it can mask severe programming errors."]}),"\n",(0,t.jsxs)(n.p,{children:["When dealing with ambiguous data type use ",(0,t.jsx)(n.code,{children:"unknown"}),", which is the type-safe counterpart of ",(0,t.jsx)(n.code,{children:"any"}),".",(0,t.jsx)(n.br,{}),"\n",(0,t.jsx)(n.code,{children:"unknown"})," doesn't allow dereferencing all properties (anything can be assigned to ",(0,t.jsx)(n.code,{children:"unknown"}),", but ",(0,t.jsx)(n.code,{children:"unknown"})," isn\u2019t assignable to anything)."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h3,{id:"type--non-nullability-assertions",children:"Type & Non-nullability Assertions"}),"\n",(0,t.jsxs)(n.p,{children:["Type assertions ",(0,t.jsx)(n.code,{children:"user as User"})," and non-nullability assertions ",(0,t.jsx)(n.code,{children:"user!.name"})," are unsafe. Both only silence TypeScript compiler and increase the risk of crashing application at runtime.",(0,t.jsx)(n.br,{}),"\n","They can only be used as an exception (e.g. third party library types mismatch, dereferencing ",(0,t.jsx)(n.code,{children:"unknown"})," etc.) with strong rational why being introduced into codebase."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h3,{id:"type-error",children:"Type Error"}),"\n",(0,t.jsxs)(n.p,{children:["If TypeScript error can't be mitigated, as last resort use ",(0,t.jsx)(n.code,{children:"@ts-expect-error"})," to suppress it (",(0,t.jsx)(n.a,{href:"https://typescript-eslint.io/rules/prefer-ts-expect-error/",children:"eslint rule"}),"). If at any future point suppressed line becomes error-free, TypeScript compiler will indicate it.",(0,t.jsx)(n.br,{}),"\n",(0,t.jsx)(n.code,{children:"@ts-ignore"})," is not allowed, where ",(0,t.jsx)(n.code,{children:"@ts-expect-error"})," must be used with provided description (",(0,t.jsx)(n.a,{href:"https://typescript-eslint.io/rules/ban-ts-comment/#allow-with-description",children:"eslint rule"}),")."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid @ts-ignore\n// @ts-ignore\nconst newUser = createUser('Gabriel');\n\n// \u2705 Use @ts-expect-error with description\n// @ts-expect-error: The library definition is wrong, createUser accepts string as an argument.\nconst newUser = createUser('Gabriel');\n"})}),"\n",(0,t.jsx)(n.h3,{id:"type-definition",children:"Type Definition"}),"\n",(0,t.jsxs)(n.p,{children:["TypeScript offers two options for type definitions - ",(0,t.jsx)(n.code,{children:"type"})," and ",(0,t.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,t.jsxs)(n.p,{children:["All types must be defined with ",(0,t.jsx)(n.code,{children:"type"})," alias ",(0,t.jsx)(n.a,{href:"https://typescript-eslint.io/rules/consistent-type-definitions/#type",children:"(eslint rule)"}),"."]}),"\n",(0,t.jsx)(r.vu,{children:(0,t.jsx)(n.p,{children:"Consider using interfaces if developing package that can be further extended, team is more comfortable working with\ninterfaces etc. In such case disable lint rule where needed e.g. using type unions (type Status = 'loading' | 'error')\netc."})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsxs)(n.p,{children:["In case of declaration merging (e.g. extending third-party library types) use ",(0,t.jsx)(n.code,{children:"interface"})," and disable lint rule."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h3,{id:"array-types",children:"Array Types"}),"\n",(0,t.jsxs)(n.p,{children:["Array types must be defined with generic syntax (",(0,t.jsx)(n.a,{href:"https://typescript-eslint.io/rules/array-type/#generic",children:"eslint rule"}),")."]}),"\n",(0,t.jsx)(r.vu,{children:(0,t.jsx)(n.p,{children:"As there is no functional difference between 'generic' and 'array' definition, feel free to set the one your team\nfinds most readable to work with."})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h2,{id:"functions",children:"Functions"}),"\n",(0,t.jsx)(n.p,{children:"Function conventions should be followed as much as possible (some of the conventions derives from functional programming basic concepts):"}),"\n",(0,t.jsx)(n.h3,{id:"general",children:"General"}),"\n",(0,t.jsx)(n.p,{children:"Function:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"should have single responsibility."}),"\n",(0,t.jsx)(n.li,{children:"should be stateless where the same input arguments return same value every single time."}),"\n",(0,t.jsx)(n.li,{children:"should accept at least one argument and return data."}),"\n",(0,t.jsx)(n.li,{children:"should not have side effects, but be pure. It's implementation should not modify or access variable value outside its local environment (global state, fetching etc.)."}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"single-object-arg",children:"Single Object Arg"}),"\n",(0,t.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,t.jsx)(n.br,{}),"\n","As exception this does not apply when having only one primitive single arg (e.g. simple functions isNumber(value), implementing currying etc.)."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h3,{id:"required--optional-args",children:"Required & Optional Args"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Strive to have majority of args required and use optional sparingly."}),(0,t.jsx)(n.br,{}),"\n","If function becomes to complex it probably should be broken into smaller pieces.",(0,t.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,t.jsx)(n.h3,{id:"args-as-discriminated-union",children:"Args as Discriminated Union"}),"\n",(0,t.jsxs)(n.p,{children:["When applicable use ",(0,t.jsx)(n.strong,{children:"discriminated union type"})," to eliminate optional args, which will decrease complexity on function API and only necessary/required args will be passed depending on its use case."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid optional args 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 args required, if that's not possible,\n// use discriminated union for clear intent on function usage\ntype StatusParamsSuccess = {\n status: 'success';\n data: Products;\n title: string;\n};\n\ntype StatusParamsLoading = {\n status: 'loading';\n time: number;\n};\n\ntype StatusParamsError = {\n status: 'error';\n error: string;\n};\n\ntype StatusParams = StatusSuccess | StatusLoading | StatusError;\n\nexport const parseStatus = (params: StatusParams) => {...\n"})}),"\n",(0,t.jsx)(n.h2,{id:"variables",children:"Variables"}),"\n",(0,t.jsx)(n.h3,{id:"const-assertion",children:"Const Assertion"}),"\n",(0,t.jsxs)(n.p,{children:["Strive to use const assertion ",(0,t.jsx)(n.code,{children:"as const"}),":"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsx)(n.p,{children:"type is narrowed"}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:["object gets ",(0,t.jsx)(n.code,{children:"readonly"})," properties"]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:["array becomes ",(0,t.jsx)(n.code,{children:"readonly"})," tuple"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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;\nconst BAR_LOCATION = [50, 130]; // Type number[]\nBAR_LOCATION.push(10);\nconst RATE_LIMIT = 25;\n// RATE_LIMIT_MESSAGE type - string\nconst RATE_LIMIT_MESSAGE = `Rate limit exceeded! Max number of requests/min is ${RATE_LIMIT}.`;\n\n\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\nconst BAR_LOCATION = [50, 130] as const; // Type 'readonly [10, 20]'\nBAR_LOCATION.push(10); // Error\nconst RATE_LIMIT = 25;\n // RATE_LIMIT_MESSAGE type - 'Rate limit exceeded! Max number of requests/min is 25.'\nconst RATE_LIMIT_MESSAGE = `Rate limit exceeded! Max number of requests/min is ${RATE_LIMIT}.` as const;\n"})}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:["exhaustiveness check, code implements all possible type values (",(0,t.jsx)(n.a,{href:"https://typescript-eslint.io/rules/switch-exhaustiveness-check/",children:"eslint rule"}),")"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"const shapes = [\n { kind: 'square', size: 7 },\n { kind: 'rectangle', width: 12, height: 6 },\n { kind: 'circle', radius: 23 },\n] as const;\n\ntype Shape = (typeof shapes)[number];\n\nconst getShapeArea = (shape: Shape) => {\n // Error - Switch is not exhaustive. Cases not matched: \"circle\"\n switch (shape.kind) {\n case 'square':\n return shape.size * shape.size;\n case 'rectangle':\n return shape.width * shape.size; // Error - Property 'size' does not exist on type 'rectangle'\n }\n};\n"})}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"enums--const-assertion",children:"Enums & Const Assertion"}),"\n",(0,t.jsx)(n.p,{children:"Const assertion must be used over enum."}),"\n",(0,t.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,t.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls",children:"Const enum pitfalls"}),", ",(0,t.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums",children:"Objects vs Enums"}),", ",(0,t.jsx)(n.a,{href:"https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings",children:"Reverse mappings"}),"..."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.h3,{id:"null--undefined",children:"Null & Undefined"}),"\n",(0,t.jsxs)(n.p,{children:["In TypeScript types ",(0,t.jsx)(n.code,{children:"null"})," and ",(0,t.jsx)(n.code,{children:"undefined"})," many times can be used interchangeably.",(0,t.jsx)(n.br,{}),"\n","Strive to:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Use ",(0,t.jsx)(n.code,{children:"null"})," to explicitly state it has no value - assignment, return function type etc."]}),"\n",(0,t.jsxs)(n.li,{children:["Use ",(0,t.jsx)(n.code,{children:"undefined"})," assignment when the value doesn't exist. E.g. exclude fields in form, request payload, database query (",(0,t.jsx)(n.a,{href:"https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined",children:"Prisma differentiation"}),")..."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"naming",children:"Naming"}),"\n",(0,t.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,t.jsx)(n.h3,{id:"named-export",children:"Named Export"}),"\n",(0,t.jsxs)(n.p,{children:["Named exports must be used to ensure that all imports follow a uniform pattern (",(0,t.jsx)(n.a,{href:"https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-default-export.md",children:"eslint rule"}),").",(0,t.jsx)(n.br,{}),"\n","This keeps variables, functions... names consistent across the entire codebase.",(0,t.jsx)(n.br,{}),"\n","Named exports have the benefit of erroring when import statements try to import something that hasn't been declared."]}),"\n",(0,t.jsx)(n.p,{children:"In case of exceptions e.g. Next.js pages, disable rule:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:'// .eslintrc.js\noverrides: [\n {\n files: ["src/pages/**/*"],\n rules: { "import/no-default-export": "off" },\n },\n],\n'})}),"\n",(0,t.jsx)(n.h3,{id:"naming-conventions",children:"Naming Conventions"}),"\n",(0,t.jsx)(n.p,{children:"While it's often hard to find the best name, try optimize code for consistency and future reader by following conventions:"}),"\n",(0,t.jsx)(n.h4,{id:"variables-1",children:"Variables"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Locals"}),(0,t.jsx)(n.br,{}),"\n","Camel case",(0,t.jsx)(n.br,{}),"\n",(0,t.jsx)(n.code,{children:"products"}),", ",(0,t.jsx)(n.code,{children:"productsFiltered"})]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Booleans"}),(0,t.jsx)(n.br,{}),"\n","Prefixed with ",(0,t.jsx)(n.code,{children:"is"}),", ",(0,t.jsx)(n.code,{children:"has"})," etc.",(0,t.jsx)(n.br,{}),"\n",(0,t.jsx)(n.code,{children:"isDisabled"}),", ",(0,t.jsx)(n.code,{children:"hasProduct"})]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Constants"}),(0,t.jsx)(n.br,{}),"\n","Capitalized",(0,t.jsx)(n.br,{}),"\n",(0,t.jsx)(n.code,{children:"PRODUCT_ID"})]}),"\n"]}),"\n",(0,t.jsxs)(n.li,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Object constants"})}),"\n",(0,t.jsx)(n.p,{children:"Singular, capitalized with const assertion."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsxs)(n.p,{children:["If object type exist use ",(0,t.jsx)(n.code,{children:"satisfies"})," operator, to verify you object constant matches its type."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"type OrderStatus = {\n pending: 'pending' | 'idle';\n fulfilled: boolean;\n error: string;\n};\n\nconst ORDER_STATUS = {\n pending: 'pending',\n fulfilled: true,\n error: 'Shipping Error',\n} as const satisfies OrderStatus;\n"})}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"functions-1",children:"Functions"}),"\n",(0,t.jsxs)(n.p,{children:["Camel case",(0,t.jsx)(n.br,{}),"\n",(0,t.jsx)(n.code,{children:"filterProductsByType"}),", ",(0,t.jsx)(n.code,{children:"formatCurrency"})]}),"\n",(0,t.jsx)(n.h4,{id:"generics",children:"Generics"}),"\n",(0,t.jsxs)(n.p,{children:["A name starts with the capital letter T ",(0,t.jsx)(n.code,{children:"TRequest"}),", ",(0,t.jsx)(n.code,{children:"TFooBar"})," (similar to ",(0,t.jsx)(n.a,{href:"https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2?view=net-5.0",children:".Net internal"})," implementation).",(0,t.jsx)(n.br,{}),"\n","Avoid (popular convention) naming generics with one character ",(0,t.jsx)(n.code,{children:"T"}),", ",(0,t.jsx)(n.code,{children:"K"})," etc., the more variables we introduce, the easier it is to mistake them."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid naming generics with one character\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\nconst createPair = (first: TFirst, second: TSecond): [TFirst, TSecond] => {\n return [first, second];\n};\nconst pair = createPair(1, 'a');\n"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"https://typescript-eslint.io/rules/naming-convention",children:"Eslint rule"})," implements:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// .eslintrc.js\n'@typescript-eslint/naming-convention': [\n 'error',\n {\n selector: 'typeParameter',\n format: ['PascalCase'],\n custom: { regex: '^T[A-Z]', match: true },\n },\n],\n"})}),"\n",(0,t.jsx)(n.h4,{id:"abbreviations--acronyms",children:"Abbreviations & Acronyms"}),"\n",(0,t.jsx)(n.p,{children:"Treat acronyms as whole words, with capitalized first letter only."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.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,t.jsx)(n.p,{children:"In favor of readability, strive to avoid abbreviations, unless they are widely accepted and necessary."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-ts",children:"// \u274c Avoid\nconst GetWin(params) => {...}\n\n// \u2705 Use\nconst GetWindow(params) => {...}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"react-components",children:"React Components"}),"\n",(0,t.jsxs)(n.p,{children:["Pascal case",(0,t.jsx)(n.br,{}),"\n",(0,t.jsx)(n.code,{children:"ProductItem"}),", ",(0,t.jsx)(n.code,{children:"ProductsPage"})]}),"\n",(0,t.jsx)(n.h4,{id:"prop-types",children:"Prop Types"}),"\n",(0,t.jsxs)(n.p,{children:['React component name following "Props" postfix',(0,t.jsx)(n.br,{}),"\n",(0,t.jsx)(n.code,{children:"[ComponentName]Props"})," - ",(0,t.jsx)(n.code,{children:"ProductItemProps"}),", ",(0,t.jsx)(n.code,{children:"ProductsPageProps"})]}),"\n",(0,t.jsx)(n.h4,{id:"callback-props",children:"Callback Props"}),"\n",(0,t.jsxs)(n.p,{children:["Event handler (callback) props are prefixed as ",(0,t.jsx)(n.code,{children:"on*"})," - e.g. ",(0,t.jsx)(n.code,{children:"onClick"}),".",(0,t.jsx)(n.br,{}),"\n","Event handler implementation functions are prefixed as ",(0,t.jsx)(n.code,{children:"handle*"})," - e.g. ",(0,t.jsx)(n.code,{children:"handleClick"})," (",(0,t.jsx)(n.a,{href:"https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-handler-names.md",children:"eslint rule"}),")."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-tsx",children:"// \u274c Avoid inconsistent callback prop naming\n +
// ❌ Avoid
const userEndpoint = '/api/usersss'; // Type 'string' - Since typo, route 'usersss' doesn't exist it will result in runtime error
// ✅ Use
type ApiRoute = 'users' | 'posts' | 'comments';
type ApiEndpoint = `/api/${ApiRoute}`;
const userEndpoint: ApiEndpoint = '/api/users';

// ❌ Avoid
const homeTitleTranslation = 'translation.homesss.title'; // Type 'string' - Since typo, translations page 'homesss' doesn't exist it will result in runtime error
// ✅ Use
type LocaleKeyPages = 'home' | 'about' | 'contact';
type TranslationKey = `translation.${LocaleKeyPages}.${string}`;
const homeTitleTranslation: TranslationKey = 'translation.home.title';

// ❌ Avoid
const color = 'blue-450'; // Type 'string' - Since color 'blue-450' doesn't exist it will result in runtime error
// ✅ Use
type BaseColor = 'blue' | 'red' | 'yellow' | 'gray';
type Variant = 50 | 100 | 200 | 300 | 400;
// Type blue-50, blue-100, blue-200, blue-300, blue-400, red-50, red-100, #AD3128 ...
type Color = `${BaseColor}-${Variant}` | `#${string}`;

Type any & unknown

any data type must not be used as it represents literally “any” value that TypeScript defaults to and skips type checking since it cannot infer the type. As such, any is dangerous, it can mask severe programming errors.

When dealing with ambiguous data type use unknown, which is the type-safe counterpart of any.
@@ -273,7 +273,7 @@

Container
ProductsPage/
├─ api/
│ └─ useGetProducts/
├─ components/
│ └─ ProductItem/
├─ utils/
│ └─ filterProductsByType/
└─ index.tsx
+
ProductsPage/
├─ api/
│ └─ useGetProducts/
├─ components/
│ └─ ProductItem/
├─ utils/
│ └─ filterProductsByType/
└─ index.tsx

UI - Feature

diff --git a/search.html b/search.html index 12f2d62d..3ed551eb 100644 --- a/search.html +++ b/search.html @@ -4,7 +4,7 @@ Search the documentation | - +