56 | );
57 | };
58 |
59 | const ExternalLinkIcon = () => (
60 |
80 | );
81 |
--------------------------------------------------------------------------------
/website/src/components/TableOfContents.tsx:
--------------------------------------------------------------------------------
1 | import type { TOCItem } from '@docusaurus/mdx-loader';
2 | import TOCInline from '@theme/TOCInline';
3 | import { useState } from 'react';
4 |
5 | type TableOfContentsProps = {
6 | items: ReadonlyArray;
7 | };
8 |
9 | export const TableOfContents = ({ items }: TableOfContentsProps) => {
10 | const [isTocExpanded, setIsTocExpanded] = useState(false);
11 |
12 | return (
13 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/website/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './GithubStats';
2 | export * from './MainTitle';
3 | export * from './Note';
4 | export * from './Rule';
5 | export * from './TableOfContents';
6 |
--------------------------------------------------------------------------------
/website/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useDocumentTitle';
2 |
--------------------------------------------------------------------------------
/website/src/hooks/useDocumentTitle.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | /**
4 | * Update the document title dynamically based on hash and title.
5 | */
6 | export const useDocumentTitle = (title: string) => {
7 | useEffect(() => {
8 | const handlePopstate = () => {
9 | const hash = window.location.hash;
10 |
11 | if (!hash) {
12 | document.title = title;
13 | return;
14 | }
15 |
16 | document.title = `${formatHash(hash)} | ${title}`;
17 | };
18 |
19 | handlePopstate();
20 |
21 | window.addEventListener('popstate', handlePopstate);
22 | return () => {
23 | window.removeEventListener('popstate', handlePopstate);
24 | };
25 | }, [title]);
26 | };
27 |
28 | const formatHash = (hash: string) => {
29 | const hashParsed = hash
30 | .substring(1)
31 | .replace(/---/g, '__dash__')
32 | .replace(/--/g, ' & ')
33 | .replace(/-/g, ' ')
34 | .replace(/__dash__/g, ' - ');
35 |
36 | const hashFormatted = toUpperEachWord(hashParsed);
37 | return hashFormatted;
38 | };
39 |
40 | const toUpperEachWord = (text: string) =>
41 | text
42 | .toLowerCase()
43 | .split(' ')
44 | .map((word) => word[0]?.toUpperCase() + word.substring(1))
45 | .join(' ');
46 |
47 | type UseDocumentTitleProps = {
48 | children: string;
49 | };
50 |
51 | /**
52 | * Wrapper component, since MDX file can't use hook directly.
53 | */
54 | export const UseDocumentTitle = ({ children }: UseDocumentTitleProps) => {
55 | useDocumentTitle(children);
56 |
57 | return null;
58 | };
59 |
--------------------------------------------------------------------------------
/website/src/pages/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: TypeScript Style Guide
3 | description: TypeScript Style Guide provides a concise set of conventions and best practices for creating consistent, maintainable code.
4 | toc_min_heading_level: 2
5 | toc_max_heading_level: 2
6 | ---
7 |
8 | import { MainTitle, Note, Rule, TableOfContents } from '@site/src/components';
9 | import { UseDocumentTitle } from '@site/src/hooks';
10 |
11 | TypeScript Style Guide
12 | TypeScript Style Guide
13 |
14 | ## Introduction
15 |
16 | TypeScript Style Guide provides a concise set of conventions and best practices for creating consistent, maintainable code.
17 |
18 | ## Table of Contents
19 |
20 |
21 |
22 | ## About Guide
23 |
24 | ### What
25 |
26 | Since "consistency is the key", TypeScript Style Guide strives to enforce the majority of rules using automated tools such as ESLint, TypeScript, Prettier, etc.
27 | However, certain design and architectural decisions must still be followed, as described in the conventions below.
28 |
29 | ### Why
30 |
31 | - As project grow in size and complexity, maintaining code quality and ensuring consistent practices become increasingly challenging.
32 | - Defining and following a standard approach to writing TypeScript applications leads to a consistent codebase and faster development cycles.
33 | - No need to discuss code styles during code reviews.
34 | - Saves team time and energy.
35 |
36 | ### Disclaimer
37 |
38 | Like any code style guide, this one is opinionated, setting conventions (sometimes arbitrary) to govern our code.
39 |
40 | 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.
41 |
42 | ### Requirements
43 |
44 | This Style Guide requires:
45 |
46 | - [TypeScript v5](https://github.com/microsoft/TypeScript)
47 | - [typescript-eslint v8](https://github.com/typescript-eslint/typescript-eslint) with [`strict-type-checked`](https://typescript-eslint.io/linting/configs/#strict-type-checked) configuration enabled.
48 |
49 | The Style Guide assumes but is not limited to using:
50 |
51 | - [React](https://github.com/facebook/react) for frontend conventions
52 | - [Playwright](https://playwright.dev/) and [Vitest](https://vitest.dev/) for testing conventions
53 |
54 | ## TLDR
55 |
56 | - **Embrace const assertions** for type safety and immutability. [⭣](#const-assertion)
57 | - Strive for **data immutability** using types like `Readonly` and `ReadonlyArray`. [⭣](#data-immutability)
58 | - Make the **majority of object properties required** (use optional properties sparingly). [⭣](#required--optional-object-properties)
59 | - **Embrace discriminated unions**. [⭣](#discriminated-union)
60 | - **Avoid type assertions** in favor of proper type definitions. [⭣](#type--non-nullability-assertions)
61 | - Strive for functions to be **pure**, **stateless**, and have **single responsibility**. [⭣](#functions)
62 | - Maintain **consistent and readable naming conventions** throughout the codebase. [⭣](#naming-conventions)
63 | - Use **named exports**. [⭣](#named-export)
64 | - **Organize code by feature** and collocate related code as close as possible. [⭣](#code-collocation)
65 |
66 | ## Types
67 |
68 | When creating types, consider how they would best **describe our code**.
69 | Being expressive and keeping types as **narrow as possible** offers several benefits to the codebase:
70 |
71 | - Increased Type Safety - Catch errors at compile time, as narrowed types provide more specific information about the shape and behavior of your data.
72 | - Improved Code Clarity - Reduces cognitive load by providing clearer boundaries and constraints on your data, making your code easier for other developers to understand.
73 | - Easier Refactoring - With narrower types, making changes to your code becomes less risky, allowing you to refactor with confidence.
74 | - Optimized Performance - In some cases, narrow types can help the TypeScript compiler generate more optimized JavaScript code.
75 |
76 | ### Type Inference
77 |
78 | As a rule of thumb, explicitly declare types only when it helps to narrow them.
79 |
80 |
81 | Just because you don't need to add types doesn't mean you shouldn't. In some cases, explicitly declaring types can
82 | improve code readability and clarify intent.
83 |
84 |
85 | Explicitly declare types when doing so helps to narrow them:
86 |
87 | ```ts
88 | // ❌ Avoid
89 | const employees = new Map(); // Inferred as wide type 'Map'
90 | employees.set('Lea', 17);
91 | type UserRole = 'admin' | 'guest';
92 | const [userRole, setUserRole] = useState('admin'); // Inferred as 'string', not the desired narrowed literal type
93 |
94 | // ✅ Use explicit type declarations to narrow the types.
95 | const employees = new Map(); // Narrowed to 'Map'
96 | employees.set('Gabriel', 32);
97 | type UserRole = 'admin' | 'guest';
98 | const [userRole, setUserRole] = useState('admin'); // Explicit type 'UserRole'
99 | ```
100 |
101 | Avoid explicitly declaring types when they can be inferred:
102 |
103 | ```ts
104 | // ❌ Avoid
105 | const userRole: string = 'admin'; // Inferred as wide type 'string'
106 | const employees = new Map([['Gabriel', 32]]); // Redundant type declaration
107 | const [isActive, setIsActive] = useState(false); // Redundant, inferred as 'boolean'
108 |
109 | // ✅ Use type inference.
110 | const USER_ROLE = 'admin'; // Inferred as narrowed string literal type 'admin'
111 | const employees = new Map([['Gabriel', 32]]); // Inferred as 'Map'
112 | const [isActive, setIsActive] = useState(false); // Inferred as 'boolean'
113 | ```
114 |
115 | ### Data Immutability
116 |
117 | Immutability should be a key principle. Wherever possible, data should remain immutable by leveraging types like `Readonly` and `ReadonlyArray`.
118 |
119 | - Using readonly types prevents accidental data mutations and reduces the risk of bugs caused by unintended side effects, ensuring data integrity throughout the application lifecycle.
120 | - 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.
121 | - Use mutations sparingly, only in cases where they are truly necessary, such as when dealing with complex objects or optimizing for performance.
122 |
123 | ```ts
124 | // ❌ Avoid data mutations
125 | const removeFirstUser = (users: Array) => {
126 | if (users.length === 0) {
127 | return users;
128 | }
129 | return users.splice(1);
130 | };
131 |
132 | // ✅ Use readonly type to prevent accidental mutations
133 | const removeFirstUser = (users: ReadonlyArray) => {
134 | if (users.length === 0) {
135 | return users;
136 | }
137 | return users.slice(1);
138 | // Using arr.splice(1) errors - Function 'splice' does not exist on 'users'
139 | };
140 | ```
141 |
142 | ### Required & Optional Object Properties
143 |
144 | **Strive to have the majority of object properties required and use optional properties sparingly.**
145 |
146 | This approach reflects designing type-safe and maintainable code:
147 |
148 | - 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.
149 | - Type Safety - When properties are required, TypeScript can enforce their presence at compile time. This prevents runtime errors caused by missing properties.
150 | - Avoids Overuse of Optional Chaining - If too many properties are optional, it often leads to extensive use of optional chaining (`?.`) to handle potential undefined values. This clutters the code and obscures its intent.
151 |
152 | If introducing many optional properties truly can't be avoided, utilize **discriminated union types**.
153 |
154 | ```ts
155 | // ❌ Avoid optional properties when possible, as they increase complexity and ambiguity
156 | type User = {
157 | id?: number;
158 | email?: string;
159 | dashboardAccess?: boolean;
160 | adminPermissions?: ReadonlyArray;
161 | subscriptionPlan?: 'free' | 'pro' | 'premium';
162 | rewardsPoints?: number;
163 | temporaryToken?: string;
164 | };
165 |
166 | // ✅ Prefer required properties. If optional properties are unavoidable,
167 | // use a discriminated union to make object usage explicit and predictable.
168 | type AdminUser = {
169 | role: 'admin';
170 | id: number;
171 | email: string;
172 | dashboardAccess: boolean;
173 | adminPermissions: ReadonlyArray;
174 | };
175 |
176 | type RegularUser = {
177 | role: 'regular';
178 | id: number;
179 | email: string;
180 | subscriptionPlan: 'free' | 'pro' | 'premium';
181 | rewardsPoints: number;
182 | };
183 |
184 | type GuestUser = {
185 | role: 'guest';
186 | temporaryToken: string;
187 | };
188 |
189 | // Discriminated union type 'User' ensures clear intent with no optional properties
190 | type User = AdminUser | RegularUser | GuestUser;
191 |
192 | const regularUser: User = {
193 | role: 'regular',
194 | id: 212,
195 | email: 'lea@user.com',
196 | subscriptionPlan: 'pro',
197 | rewardsPoints: 1500,
198 | dashboardAccess: false, // Error: 'dashboardAccess' property does not exist
199 | };
200 | ```
201 |
202 | ### Discriminated Union
203 |
204 | If there's only one TypeScript feature to choose from, embrace discriminated unions.
205 |
206 | Discriminated unions are a powerful concept to model complex data structures and improve type safety, leading to clearer and less error-prone code.
207 | 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).
208 |
209 | Discriminated unions advantages:
210 |
211 | - As mentioned in [Required & Optional Object Properties](#required--optional-object-properties), [Args as Discriminated Type](#args-as-discriminated-type) and [Props as Discriminated Type](#props-as-discriminated-type), discriminated unions remove optional object properties, reducing complexity.
212 | - Exhaustiveness check - TypeScript can ensure that all possible variants of a type are implemented, eliminating the risk of undefined or unexpected behavior at runtime.
213 |
214 | {`"@typescript-eslint/switch-exhaustiveness-check": "error"`}
215 |
216 | ```ts
217 | type Circle = { kind: 'circle'; radius: number };
218 | type Square = { kind: 'square'; size: number };
219 | type Triangle = { kind: 'triangle'; base: number; height: number };
220 |
221 | // Create discriminated union 'Shape', with 'kind' property to discriminate the type of object.
222 | type Shape = Circle | Square | Triangle;
223 |
224 | // TypeScript warns us with errors in calculateArea function
225 | const calculateArea = (shape: Shape) => {
226 | // Error - Switch is not exhaustive. Cases not matched: "triangle"
227 | switch (shape.kind) {
228 | case 'circle':
229 | return Math.PI * shape.radius ** 2;
230 | case 'square':
231 | return shape.size * shape.width; // Error - Property 'width' does not exist on type 'square'
232 | }
233 | };
234 | ```
235 |
236 | - Avoid code complexity introduced by [flag variables](#type-union--boolean-flags).
237 | - Clear code intent, as it becomes easier to read and understand by explicitly indicating the possible cases for a given type.
238 | - TypeScript can narrow down union types, ensuring code correctness at compile time.
239 | - 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.
240 | - IDEs can leverage discriminated unions to provide better autocompletion and type inference.
241 |
242 | ### Type-Safe Constants With Satisfies
243 |
244 | The `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.
245 |
246 | Key benefits:
247 |
248 | - Immutability with `as const`
249 | - Ensures the constant is treated as readonly.
250 | - Narrows the types of values to their literals, preventing accidental modifications.
251 | - Validation with `satisfies`
252 | - Ensures the object conforms to a broader type without widening its inferred type.
253 | - Helps catch type mismatches at compile time while preserving narrowed inferred types.
254 |
255 | Array constants:
256 |
257 | ```ts
258 | type UserRole = 'admin' | 'editor' | 'moderator' | 'viewer' | 'guest';
259 |
260 | // ❌ Avoid constant of wide type
261 | const DASHBOARD_ACCESS_ROLES: ReadonlyArray = ['admin', 'editor', 'moderator'];
262 |
263 | // ❌ Avoid constant with incorrect values
264 | const DASHBOARD_ACCESS_ROLES = ['admin', 'contributor', 'analyst'] as const;
265 |
266 | // ✅ Use immutable constant of narrowed type
267 | const DASHBOARD_ACCESS_ROLES = ['admin', 'editor', 'moderator'] as const satisfies ReadonlyArray;
268 | ```
269 |
270 | Object constants:
271 |
272 | ```ts
273 | type OrderStatus = {
274 | pending: 'pending' | 'idle';
275 | fulfilled: boolean;
276 | error: string;
277 | };
278 |
279 | // ❌ Avoid mutable constant of wide type
280 | const IDLE_ORDER: OrderStatus = {
281 | pending: 'idle',
282 | fulfilled: true,
283 | error: 'Shipping Error',
284 | };
285 |
286 | // ❌ Avoid constant with incorrect values
287 | const IDLE_ORDER = {
288 | pending: 'done',
289 | fulfilled: 'partially',
290 | error: 116,
291 | } as const;
292 |
293 | // ✅ Use immutable constant of narrowed type
294 | const IDLE_ORDER = {
295 | pending: 'idle',
296 | fulfilled: true,
297 | error: 'Shipping Error',
298 | } as const satisfies OrderStatus;
299 | ```
300 |
301 | ### Template Literal Types
302 |
303 | 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.
304 |
305 | Adopting template literal types brings several advantages:
306 |
307 | - Prevent errors caused by typos or invalid strings.
308 | - Provide better type safety and autocompletion support.
309 | - Improve code maintainability and readability.
310 |
311 | Template literal types are useful in various practical scenarios, such as:
312 |
313 | - String Patterns - Use template literal types to enforce valid string patterns.
314 |
315 | ```ts
316 | // ❌ Avoid
317 | const appVersion = '2.6';
318 | // ✅ Use
319 | type Version = `v${number}.${number}.${number}`;
320 | const appVersion: Version = 'v2.6.1';
321 | ```
322 |
323 | - API Endpoints - Use template literal types to restrict values to valid API routes.
324 |
325 | ```ts
326 | // ❌ Avoid
327 | const userEndpoint = '/api/usersss'; // Type 'string' - Typo 'usersss': the route doesn't exist, leading to a runtime error.
328 | // ✅ Use
329 | type ApiRoute = 'users' | 'posts' | 'comments';
330 | type ApiEndpoint = `/api/${ApiRoute}`; // Type ApiEndpoint = "/api/users" | "/api/posts" | "/api/comments"
331 | const userEndpoint: ApiEndpoint = '/api/users';
332 | ```
333 |
334 | - 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.
335 |
336 | ```ts
337 | // ❌ Avoid
338 | const homeTitle = 'translation.homesss.title'; // Type 'string' - Typo 'homesss': the translation doesn't exist, leading to a runtime error.
339 | // ✅ Use
340 | type LocaleKeyPages = 'home' | 'about' | 'contact';
341 | type TranslationKey = `translation.${LocaleKeyPages}.${string}`; // Type TranslationKey = `translation.home.${string}` | `translation.about.${string}` | `translation.contact.${string}`
342 | const homeTitle: TranslationKey = 'translation.home.title';
343 | ```
344 |
345 | - 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.
346 |
347 | ```ts
348 | // ❌ Avoid
349 | const color = 'blue-450'; // Type 'string' - Color 'blue-450' doesn't exist, leading to a runtime error.
350 | // ✅ Use
351 | type BaseColor = 'blue' | 'red' | 'yellow' | 'gray';
352 | type Variant = 50 | 100 | 200 | 300 | 400;
353 | type Color = `${BaseColor}-${Variant}` | `#${string}`; // Type Color = "blue-50" | "blue-100" | "blue-200" ... | "red-50" | "red-100" ... | #${string}
354 | const iconColor: Color = 'blue-400';
355 | const customColor: Color = '#AD3128';
356 | ```
357 |
358 | - 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.
359 |
360 |
361 | ```ts
362 | // ❌ Avoid
363 | const query = 'SELECT name FROM usersss WHERE age > 30'; // Type 'string' - Typo 'usersss': table doesn't exist, leading to a runtime error.
364 | // ✅ Use
365 | type Table = 'users' | 'posts' | 'comments';
366 | type Column =
367 | TTableName extends 'users' ? 'id' | 'name' | 'age' :
368 | TTableName extends 'posts' ? 'id' | 'title' | 'content' :
369 | TTableName extends 'comments' ? 'id' | 'postId' | 'text' :
370 | never;
371 |
372 | type Query = `SELECT ${Column} FROM ${TTableName} WHERE ${string}`;
373 | const userQuery: Query<'users'> = 'SELECT name FROM users WHERE age > 30'; // Valid query
374 | const invalidQuery: Query<'users'> = 'SELECT title FROM users WHERE age > 30'; // Error: 'title' is not a column in 'users' table.
375 | ```
376 |
377 |
378 | ### Type any & unknown
379 |
380 | `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.
381 |
382 | When dealing with ambiguous data type use `unknown`, which is the type-safe counterpart of `any`.
383 | `unknown` doesn't allow dereferencing all properties (anything can be assigned to `unknown`, but `unknown` isn’t assignable to anything).
384 |
385 | ```ts
386 | // ❌ Avoid any
387 | const foo: any = 'five';
388 | const bar: number = foo; // no type error
389 |
390 | // ✅ Use unknown
391 | const foo: unknown = 5;
392 | const bar: number = foo; // type error - Type 'unknown' is not assignable to type 'number'
393 |
394 | // Narrow the type before dereferencing it using:
395 | // Type guard
396 | const isNumber = (num: unknown): num is number => {
397 | return typeof num === 'number';
398 | };
399 | if (!isNumber(foo)) {
400 | throw Error(`API provided a fault value for field 'foo':${foo}. Should be a number!`);
401 | }
402 | const bar: number = foo;
403 |
404 | // Type assertion
405 | const bar: number = foo as number;
406 | ```
407 |
408 | ### Type & Non-nullability Assertions
409 |
410 | Type assertions `user as User` and non-nullability assertions `user!.name` are unsafe. Both only silence TypeScript compiler and increase the risk of crashing application at runtime.
411 | They can only be used as an exception (e.g. third party library types mismatch, dereferencing `unknown` etc.) with a strong rational for why it's introduced into the codebase.
412 |
413 | ```ts
414 | type User = { id: string; username: string; avatar: string | null };
415 | // ❌ Avoid type assertions
416 | const user = { name: 'Nika' } as User;
417 | // ❌ Avoid non-nullability assertions
418 | renderUserAvatar(user!.avatar); // Runtime error
419 |
420 | const renderUserAvatar = (avatar: string) => {...}
421 | ```
422 |
423 | ### Type Errors
424 |
425 | When a TypeScript error cannot be mitigated, use `@ts-expect-error` as a last resort to suppress it.
426 |
427 | This directive notifies the compiler when the suppressed error no longer exists, ensuring errors are revisited once they’re obsolete, unlike `@ts-ignore`, which can silently linger even after the error is resolved.
428 |
429 | - Always use `@ts-expect-error` with a clear description explaining why it is necessary.
430 | - Avoid `@ts-ignore`, as it does not track suppressed errors.
431 |
432 | {`'@typescript-eslint/ban-ts-comment': [
433 | 'error',
434 | {
435 | 'ts-expect-error': 'allow-with-description'
436 | },
437 | ]`}
438 |
439 | ```ts
440 | // ❌ Avoid @ts-ignore as it will do nothing if the following line is error-free.
441 | // @ts-ignore
442 | const newUser = createUser('Gabriel');
443 |
444 | // ✅ Use @ts-expect-error with description.
445 | // @ts-expect-error: This library function has incorrect type definitions - createUser accepts string as an argument.
446 | const newUser = createUser('Gabriel');
447 | ```
448 |
449 | ### Type Definition
450 |
451 | TypeScript provides two options for defining types: `type` and `interface`. While these options have some functional differences, they are interchangeable in most cases. To maintain consistency, choose one and use it consistently.
452 |
453 | {`'@typescript-eslint/consistent-type-definitions': ['error', 'type']`}
457 |
458 |
459 | Consider using interfaces when developing a package that might be extended by third-party consumers in the future or
460 | when your team prefers working with interfaces. In these cases, you can disable linting rules if needed, such as when
461 | defining type unions (e.g. `type Status = 'loading' | 'error'`).
462 |
463 |
464 | ```ts
465 | // ❌ Avoid interface definitions
466 | interface UserRole = 'admin' | 'guest'; // Invalid - interfaces can't define type unions
467 |
468 | interface UserInfo {
469 | name: string;
470 | role: 'admin' | 'guest';
471 | }
472 |
473 | // ✅ Use type definition
474 | type UserRole = 'admin' | 'guest';
475 |
476 | type UserInfo = {
477 | name: string;
478 | role: UserRole;
479 | };
480 |
481 | ```
482 |
483 | When performing declaration merging (e.g. extending third-party library types), use `interface` and disable the lint rule where necessary.
484 |
485 | ```ts
486 | // types.ts
487 | declare namespace NodeJS {
488 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
489 | export interface ProcessEnv {
490 | NODE_ENV: 'development' | 'production';
491 | PORT: string;
492 | CUSTOM_ENV_VAR: string;
493 | }
494 | }
495 |
496 | // server.ts
497 | app.listen(process.env.PORT, () => {...}
498 | ```
499 |
500 | ### Array Types
501 |
502 | {`'@typescript-eslint/array-type': ['error', { default: 'generic' }]`}
506 |
507 | Since there is no functional difference between the 'generic' and 'array' definitions, feel free to choose the one
508 | that your team finds most readable.
509 |
510 |
511 | ```ts
512 | // ❌ Avoid
513 | const x: string[] = ['foo', 'bar'];
514 | const y: readonly string[] = ['foo', 'bar'];
515 |
516 | // ✅ Use
517 | const x: Array = ['foo', 'bar'];
518 | const y: ReadonlyArray = ['foo', 'bar'];
519 | ```
520 |
521 | ### Type Imports and Exports
522 |
523 | TypeScript allows specifying a `type` keyword on imports to indicate that the export exists only in the type system, not at runtime.
524 |
525 | Type imports must always be separated:
526 |
527 | - Tree Shaking and Dead Code Elimination: If you use `import` for types instead of `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.
528 | - 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.
529 | - Improves code clarity by making the distinction between runtime dependencies and type-only imports explicit.
530 |
531 | {`'@typescript-eslint/consistent-type-imports': 'error'`}
532 |
533 | ```ts
534 | // ❌ Avoid using `import` for both runtime and type
535 | import { MyClass } from 'some-library';
536 |
537 | // Even if MyClass is only a type, the entire module might be included in the bundle.
538 |
539 | // ✅ Use `import type`
540 | import type { MyClass } from 'some-library';
541 |
542 | // This ensures only the type is imported and no runtime code from "some-library" ends up in the bundle.
543 | ```
544 |
545 | ### Services & Types Generation
546 |
547 | 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 protocols and databases.
548 |
549 | For external services, such as REST, GraphQL, and MQ it's crucial to generate types from their contracts, whether they use Swagger, schemas, or other sources (e.g. [openapi-ts](https://github.com/drwpow/openapi-typescript), [graphql-config](https://github.com/kamilkisiela/graphql-config)). Avoid manually declaring and maintaining types, as they can easily fall out of sync.
550 |
551 | As an exception, only manually declare types when no options are available, such as when there is no documentation for the service, data cannot be fetched to retrieve a contract, or the database cannot be accessed to infer types.
552 |
553 | ## Functions
554 |
555 | Function conventions should be followed as much as possible (some of the conventions derive from functional programming basic concepts):
556 |
557 | ### General
558 |
559 | Function:
560 |
561 | - should have single responsibility.
562 | - should be stateless where the same input arguments return same value every single time.
563 | - should accept at least one argument and return data.
564 | - should not have side effects, but be pure. Implementation should not modify or access variable value outside its local environment (global state, fetching etc.).
565 |
566 | ### Single Object Arg
567 |
568 | 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.
569 | As an exception this does not apply when having only one primitive single arg (e.g. simple functions isNumber(value), implementing currying etc.).
570 |
571 | ```ts
572 | // ❌ Avoid having multiple arguments
573 | transformUserInput('client', false, 60, 120, null, true, 2000);
574 |
575 | // ✅ Use options object as argument
576 | transformUserInput({
577 | method: 'client',
578 | isValidated: false,
579 | minLines: 60,
580 | maxLines: 120,
581 | defaultInput: null,
582 | shouldLog: true,
583 | timeout: 2000,
584 | });
585 | ```
586 |
587 | ### Required & Optional Args
588 |
589 | **Strive to have majority of args required and use optional sparingly.**
590 | If the function becomes too complex, it probably should be broken into smaller pieces.
591 | 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.
592 |
593 | ### Args as Discriminated Type
594 |
595 | When applicable use **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.
596 |
597 | ```ts
598 | // ❌ Avoid optional properties as they increase complexity and ambiguity in function APIs
599 | type StatusParams = {
600 | data?: Products;
601 | title?: string;
602 | time?: number;
603 | error?: string;
604 | };
605 |
606 | // ✅ Prefer required properties. If optional properties are unavoidable,
607 | // use a discriminated union to represent distinct use cases with required properties.
608 | type StatusSuccessParams = {
609 | status: 'success';
610 | data: Products;
611 | title: string;
612 | };
613 |
614 | type StatusLoadingParams = {
615 | status: 'loading';
616 | time: number;
617 | };
618 |
619 | type StatusErrorParams = {
620 | status: 'error';
621 | error: string;
622 | };
623 |
624 | // Discriminated union 'StatusParams' ensures predictable function arguments with no optional properties
625 | type StatusParams = StatusSuccessParams | StatusLoadingParams | StatusErrorParams;
626 |
627 | export const parseStatus = (params: StatusParams) => {...
628 | ```
629 |
630 | ### Return Types
631 |
632 | Requiring explicit return types improves safety, catches errors early, and helps with long-term maintainability. However, excessive strictness can slow development and add unnecessary redundancy.
633 |
634 | Consider the advantages of explicitly defining the return type of a function:
635 |
636 | - **Improves Readability**: Clearly specifies what type of value the function returns, making the code easier to understand for those calling the function.
637 | - **Avoids Misuse**: Ensures that calling code does not accidentally attempt to use an undefined value when no return value is intended.
638 | - **Surfaces Type Errors Early**: Helps catch potential type errors during development, especially when code changes unintentionally alter the return type.
639 | - **Simplifies Refactoring**: Ensures that any variable assigned to the function's return value is of the correct type, making refactoring safer and more efficient.
640 | - **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.
641 | - **Optimizes Compilation**: While TypeScript's type inference is powerful, explicitly defining return types can reduce the workload on the TypeScript compiler, improving overall performance.
642 |
643 | As context matters, use explicit return types when they add clarity and safety.
644 |
645 | {`"@typescript-eslint/explicit-function-return-type": "error"`}
649 |
650 | ## Variables
651 |
652 | ### Const Assertion
653 |
654 | Strive declaring constants using const assertion `as const`:
655 |
656 | 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.
657 |
658 | - Type Narrowing - Using `as const` ensures that literal values (e.g., numbers, strings) are treated as exact values instead of generalized types like `number` or `string`.
659 | - Immutability - Objects and arrays get readonly properties, preventing accidental mutations.
660 |
661 | Examples:
662 |
663 | - Objects
664 |
665 | ```ts
666 | // ❌ Avoid
667 | const FOO_LOCATION = { x: 50, y: 130 }; // Type { x: number; y: number; }
668 | FOO_LOCATION.x = 10;
669 |
670 | // ✅ Use
671 | const FOO_LOCATION = { x: 50, y: 130 } as const; // Type '{ readonly x: 50; readonly y: 130; }'
672 | FOO_LOCATION.x = 10; // Error
673 | ```
674 |
675 | - Arrays
676 |
677 | ```ts
678 | // ❌ Avoid
679 | const BAR_LOCATION = [50, 130]; // Type number[]
680 | BAR_LOCATION.push(10);
681 |
682 | // ✅ Use
683 | const BAR_LOCATION = [50, 130] as const; // Type 'readonly [10, 20]'
684 | BAR_LOCATION.push(10); // Error
685 | ```
686 |
687 | - Template Literals
688 |
689 | ```ts
690 | // ❌ Avoid
691 | const RATE_LIMIT = 25;
692 | const RATE_LIMIT_MESSAGE = `Max number of requests/min is ${RATE_LIMIT}.`; // Type string
693 |
694 | // ✅ Use
695 | const RATE_LIMIT = 25;
696 | const RATE_LIMIT_MESSAGE = `Max number of requests/min is ${RATE_LIMIT}.` as const; // Type 'Rate limit exceeded! Max number of requests/min is 25.'
697 | ```
698 |
699 | ### Enums & Const Assertion
700 |
701 | Enums are discouraged in the TypeScript ecosystem due to their runtime cost and quirks.
702 | The TypeScript documentation outlines several [pitfalls](https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls), and recently introduced the [--erasableSyntaxOnly](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-8.html#the---erasablesyntaxonly-option) flag to disable runtime-generating features like enums altogether.
703 |
704 | {`'no-restricted-syntax': [
705 | 'error',
706 | {
707 | selector: 'TSEnumDeclaration',
708 | message: 'Replace enum with a literal type or a const assertion.',
709 | },
710 | ]`}
711 |
712 | As rule of a thumb, prefer:
713 |
714 | - Literal types whenever possible.
715 | - Const assertion arrays when looping through values.
716 | - Const assertion objects when enumerating arbitrary values.
717 |
718 | Examples:
719 |
720 | - Use literal types to avoid runtime objects and reduce bundle size.
721 |
722 | ```ts
723 | // ❌ Avoid using enums as they increase the bundle size
724 | enum UserRole {
725 | GUEST = 'guest',
726 | MODERATOR = 'moderator',
727 | ADMINISTRATOR = 'administrator',
728 | }
729 |
730 | // Transpiled JavaScript
731 | ('use strict');
732 | var UserRole;
733 | (function (UserRole) {
734 | UserRole['GUEST'] = 'guest';
735 | UserRole['MODERATOR'] = 'moderator';
736 | UserRole['ADMINISTRATOR'] = 'administrator';
737 | })(UserRole || (UserRole = {}));
738 |
739 | // ✅ Use literal types - Types are stripped during transpilation
740 | type UserRole = 'guest' | 'moderator' | 'administrator';
741 |
742 | const isGuest = (role: UserRole) => role === 'guest';
743 | ```
744 |
745 | - Use const assertion arrays when looping through values.
746 |
747 | ```tsx
748 | // ❌ Avoid using enums
749 | enum USER_ROLES {
750 | guest = 'guest',
751 | moderator = 'moderator',
752 | administrator = 'administrator',
753 | }
754 |
755 | // ✅ Use const assertions arrays
756 | const USER_ROLES = ['guest', 'moderator', 'administrator'] as const;
757 | type UserRole = (typeof USER_ROLES)[number];
758 |
759 | const seedDatabase = () => {
760 | USER_ROLES.forEach((role) => {
761 | db.roles.insert(role);
762 | }
763 | }
764 | const insert = (role: UserRole) => {...
765 |
766 | const UsersRoleList = () => {
767 | return (
768 |
;
1436 |
1437 | // ❌
1438 | export const PriceList = {
1439 | Container: PriceListRoot,
1440 | Item: PriceListItem,
1441 | };
1442 | // ❌
1443 | PriceList.Item = Item;
1444 | export default PriceList;
1445 |
1446 | // ✅
1447 | export const PriceList = PriceListRoot as typeof PriceListRoot & {
1448 | Item: typeof PriceListItem;
1449 | };
1450 | PriceList.Item = PriceListItem;
1451 |
1452 | // App.tsx
1453 | import { PriceList } from "./PriceList";
1454 |
1455 |
1456 |
1457 |
1458 | ;
1459 | ```
1460 |
1461 | - UI components should show derived state and send events, nothing more (no business logic).
1462 | - As in many programming languages functions args can be passed to the next function and on to the next etc.
1463 | React components are no different, where prop drilling should not become an issue.
1464 | If with app scaling prop drilling truly becomes an issue, try to refactor render method, local states in parent components, using composition etc.
1465 | - Data fetching is only allowed in container components.
1466 | - Use of server-state library is encouraged ([react-query](https://github.com/tanstack/query), [apollo client](https://github.com/apollographql/apollo-client) etc.).
1467 | - Use of client-state library for global state is discouraged.
1468 | Reconsider if something should be truly global across application, e.g. `themeMode`, `Permissions` or even that can be put in server-state (e.g. user settings - `/me` endpoint). If still global state is truly needed use [Zustand](https://github.com/pmndrs/zustand) or [Context](https://react.dev/reference/react/createContext).
1469 |
1470 | ## Appendix - Tests
1471 |
1472 | ### What & How To Test
1473 |
1474 | Automated test comes with benefits that helps us write better code and makes it easy to refactor, while bugs are caught earlier in the process.
1475 | Consider trade-offs of what and how to test to achieve confidence application is working as intended, while writing and maintaining tests doesn't slow the team down.
1476 |
1477 | ✅ Do:
1478 |
1479 | - Implement test to be short, explicit, and pleasant to work with. Intent of a test should be immediately visible.
1480 | - Strive for AAA pattern, to maintain clean, organized, and understandable unit tests.
1481 | - Arrange - Setup preconditions or the initial state necessary for the test case. Create necessary objects and define input values.
1482 | - Act - Perform the action you want to unit test (invoke a method, triggering an event etc.). **Strive for minimal number of actions**.
1483 | - Assert - Validate the outcome against expectations. **Strive for minimal number of asserts**.
1484 | A rule "unit tests should fail for exactly one reason" doesn't need to apply always, but it can indicate a code smell if there are tests with many asserts in a codebase.
1485 | - As mentioned in [function conventions](#functions) try to keep them pure, and impure one small and focused.
1486 | It makes them easy to test, by passing args and observing return values, since we will **rarely need to mock dependencies**.
1487 | - Strive to write tests in a way your app is used by a user, meaning test business logic.
1488 | E.g. For a specific user role or permission, given some input, we receive the expected output from the process.
1489 | - Make tests as isolated as possible, where they don't depend on order of execution and should run independently with its own local storage, session storage, data, cookies etc.
1490 | Test isolation speeds up the test run, improves reproducibility, makes debugging easier and prevents cascading test failures.
1491 | - Tests should be resilient to changes.
1492 | - Black box testing - Always test only implementation that is publicly exposed, don't write fragile tests on how implementation works internally.
1493 | - Query HTML elements based on attributes that are unlikely to change. Order of priority must be followed as specified in [Testing Library](https://testing-library.com/docs/queries/about/#priority) - [role](https://testing-library.com/docs/queries/byrole), [label](https://testing-library.com/docs/queries/bylabeltext), [placeholder](https://testing-library.com/docs/queries/byplaceholdertext), [text contents](https://testing-library.com/docs/queries/bytext), [display value](https://testing-library.com/docs/queries/bydisplayvalue), [alt text](https://testing-library.com/docs/queries/byalttext), [title](https://testing-library.com/docs/queries/bytitle), [test ID](https://testing-library.com/docs/queries/bytestid).
1494 | - If testing with a database then make sure you control the data. If test are run against a staging environment make sure it doesn't change.
1495 |
1496 | ❌ Don't:
1497 |
1498 | - Don't test implementation details. When refactoring code, tests shouldn't change.
1499 | - Don't re-test the library/framework.
1500 | - Don't mandate 100% code coverage for applications.
1501 | - Don't test third-party dependencies. Only test what your team controls (package, API, microservice etc.). Don't test external sites links, third party servers, packages etc.
1502 | - Don't test just to test.
1503 |
1504 | ```ts
1505 | // ❌ Avoid
1506 | it('should render the user list', () => {
1507 | render();
1508 | expect(screen.getByText('Users List')).toBeInTheDocument();
1509 | });
1510 | ```
1511 |
1512 | ### Test Description
1513 |
1514 | All test descriptions must follow naming convention as `it('should ... when ...')`.
1515 |
1516 |
1517 | {`'vitest/valid-title': [
1518 | 'error',
1519 | {
1520 | mustMatch: { it: [/should.*when/u.source, "Test title must include 'should' and 'when'"] },
1521 | },
1522 | ]`}
1523 |
1524 |
1525 | ```ts
1526 | // ❌ Avoid
1527 | it('accepts ISO date format where date is parsed and formatted as YYYY-MM');
1528 | it('after title is confirmed user description is rendered');
1529 |
1530 | // ✅ Name test description as it('should ... when ...')
1531 | it('should return parsed date as YYYY-MM when input is in ISO date format');
1532 | it('should render user description when title is confirmed');
1533 | ```
1534 |
1535 | ### Test Tooling
1536 |
1537 | Besides running tests through scripts, it's highly encouraged to use [Vitest Runner](https://marketplace.visualstudio.com/items?itemName=vitest.explorer) and [Playwright Test](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) VS code extension alongside.
1538 | With extension any single [unit/integration](https://github.com/mkosir/typescript-style-guide/raw/main/misc/vscode-vitest-runner.gif) or [E2E](https://github.com/mkosir/typescript-style-guide/raw/main/misc/vscode-playwright-test.gif) test can be run instantly, especially if testing app or package in larger monorepo codebase.
1539 |
1540 | ```sh
1541 | code --install-extension vitest.explorer
1542 | code --install-extension ms-playwright.playwright
1543 | ```
1544 |
1545 | ### Snapshot
1546 |
1547 | Snapshot tests are discouraged to avoid fragility, which leads to a "just update it" mindset to make all tests pass.
1548 | Exceptions can be made, with strong rationale behind them, where test output has short and clear intent about what's actually being tested (e.g., design system library critical elements that shouldn't deviate).
1549 |
--------------------------------------------------------------------------------
/website/src/styles/global.css:
--------------------------------------------------------------------------------
1 | /* https://tailwindcss.com/docs/preflight#disabling-preflight */
2 | /* @import 'tailwindcss'; */
3 |
4 | @layer theme, base, components, utilities;
5 | @import 'tailwindcss/theme.css' layer(theme);
6 | /* @import 'tailwindcss/preflight.css' layer(base); */
7 | /* @import 'tailwindcss/utilities.css' layer(utilities); */
8 | @tailwind utilities;
9 |
10 | @custom-variant dark (&:is([data-theme="dark"] *));
11 |
12 | /* You can override the default Infima variables here. */
13 | :root {
14 | --ifm-color-primary: #296bb3;
15 | --ifm-color-primary-dark: #2560a1;
16 | --ifm-color-primary-darker: #235b98;
17 | --ifm-color-primary-darkest: #1d4b7d;
18 | --ifm-color-primary-light: #2d76c5;
19 | --ifm-color-primary-lighter: #2f7bce;
20 | --ifm-color-primary-lightest: #498cd5;
21 | --ifm-code-font-size: 95%;
22 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
23 | }
24 |
25 | /* For readability concerns, you should choose a lighter palette in dark mode. */
26 | [data-theme='dark'] {
27 | --ifm-color-primary: #94c8ff;
28 | --ifm-color-primary-dark: #6cb3ff;
29 | --ifm-color-primary-darker: #58a9ff;
30 | --ifm-color-primary-darkest: #1b8aff;
31 | --ifm-color-primary-light: #bcddff;
32 | --ifm-color-primary-lighter: #d0e7ff;
33 | --ifm-color-primary-lightest: #ffffff;
34 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
35 | }
36 |
37 | h1 {
38 | font-size: 30px;
39 | margin: 0;
40 | }
41 |
42 | h2 {
43 | margin-bottom: 13px;
44 | }
45 |
46 | h3 {
47 | margin-bottom: 13px;
48 | }
49 |
50 | h4 {
51 | margin-bottom: 13px;
52 | }
53 |
54 | p {
55 | margin-bottom: 15px;
56 | }
57 |
58 | button {
59 | all: unset;
60 | display: inline-flex;
61 | align-items: center;
62 | }
63 |
--------------------------------------------------------------------------------
/website/src/theme/NavbarItem/ComponentTypes.tsx:
--------------------------------------------------------------------------------
1 | import { GithubStats } from '@site/src/components';
2 | import ComponentTypes from '@theme-original/NavbarItem/ComponentTypes';
3 |
4 | // https://docusaurus.io/docs/swizzling
5 |
6 | // Theming: use custom components as navbar/sidebar/footer items
7 | // https://github.com/facebook/docusaurus/issues/7227
8 |
9 | // eslint-disable-next-line import/no-default-export
10 | export default {
11 | ...ComponentTypes,
12 | 'custom-GithubStats': GithubStats,
13 | };
14 |
--------------------------------------------------------------------------------
/website/src/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from 'clsx';
2 | import clsx from 'clsx';
3 | import { twMerge } from 'tailwind-merge';
4 |
5 | export const cn = (...inputs: ReadonlyArray) => twMerge(clsx(inputs));
6 |
--------------------------------------------------------------------------------
/website/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mkosir/typescript-style-guide/e52658ae1fa7f8b9b993019f9b3d87af2409c5a0/website/static/.nojekyll
--------------------------------------------------------------------------------
/website/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mkosir/typescript-style-guide/e52658ae1fa7f8b9b993019f9b3d87af2409c5a0/website/static/img/favicon.ico
--------------------------------------------------------------------------------
/website/static/img/typescript-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mkosir/typescript-style-guide/e52658ae1fa7f8b9b993019f9b3d87af2409c5a0/website/static/img/typescript-card.png
--------------------------------------------------------------------------------
/website/static/img/typescript-logo-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mkosir/typescript-style-guide/e52658ae1fa7f8b9b993019f9b3d87af2409c5a0/website/static/img/typescript-logo-40.png
--------------------------------------------------------------------------------
/website/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // This file is not used in compilation. It is here just for a nice editor experience.
3 | "extends": "@docusaurus/tsconfig",
4 | "compilerOptions": {
5 | "baseUrl": ".",
6 | "module": "ESNext",
7 | "moduleResolution": "Bundler",
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noUncheckedIndexedAccess": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "target": "ES2020"
18 | },
19 | "include": ["."],
20 | "exclude": ["node_modules", "build", ".docusaurus"]
21 | }
22 |
--------------------------------------------------------------------------------