@canonical/code-standards 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/code.md ADDED
@@ -0,0 +1,720 @@
1
+ # Code Standards
2
+
3
+ Standards for code development.
4
+
5
+ ## code/api/stability
6
+
7
+ Experimental APIs must be marked with the @experimental JSDoc tag in type definition files. The tag must include a description of what is experimental.
8
+
9
+ ### Do
10
+
11
+ (Do) Mark an experimental interface with a clear @experimental JSDoc tag and description.
12
+ ```typescript
13
+ /**
14
+ * Configuration for the data processing pipeline.
15
+ * @experimental The streaming API is experimental and may change
16
+ * in future releases. Currently, it only supports JSON data.
17
+ */
18
+ interface PipelineConfig {
19
+ // ...
20
+ }
21
+ ```
22
+
23
+ (Do) Add an @experimental tag to a specific property with context about its experimental status.
24
+ ```typescript
25
+ interface PipelineConfig {
26
+ /**
27
+ * @experimental The custom transformers API is in beta, and the interface
28
+ * may change to support stronger type validation.
29
+ */
30
+ transformers?: DataTransformer[];
31
+ }
32
+ ```
33
+
34
+ (Do) Describe the experimental status and any future plans for the API.
35
+ ```typescript
36
+ interface CacheConfig {
37
+ /**
38
+ * @experimental The distributed cache API is experimental and will be
39
+ * replaced with a new consensus-based implementation in v2.1.
40
+ */
41
+ distributed?: boolean;
42
+ }
43
+ ```
44
+
45
+ ### Don't
46
+
47
+ (Don't) Use the @experimental tag without an explanation.
48
+ ```typescript
49
+ interface ProcessorConfig {
50
+ /** @experimental */ // Bad: No context provided.
51
+ streaming?: boolean;
52
+ }
53
+ ```
54
+
55
+ (Don't) Use the @experimental tag without describing what is experimental.
56
+ ```typescript
57
+ /**
58
+ * @experimental // Bad: No description of what is experimental.
59
+ */
60
+ interface QueueConfig {
61
+ processor: (item: any) => Promise<void>;
62
+ }
63
+ ```
64
+
65
+ ---
66
+
67
+ ## code/constants/file
68
+
69
+ Domain constants must be collected in a `constants.ts` file using named exports (no default export). The file must be colocated at the level of the domain that owns the constants — inside the domain folder when one exists. Constants files always use named exports so consumers can import exactly what they need. Single-use constants that are only referenced in one file may be declared inline in that file instead of being extracted to `constants.ts`.
70
+
71
+ ### Do
72
+
73
+ (Do) Create a `constants.ts` file with named exports at the domain level:
74
+ ```typescript
75
+ // features/pricing/constants.ts
76
+ export const DEFAULT_CURRENCY = "USD";
77
+ export const TAX_RATE = 0.21;
78
+ export const MAX_DISCOUNT_PERCENT = 50;
79
+ ```
80
+
81
+ (Do) Colocate `constants.ts` inside the domain folder it belongs to:
82
+ ```
83
+ features/
84
+ ├── pricing/
85
+ │ ├── constants.ts # Domain constants live here
86
+ │ ├── calculatePrice.ts
87
+ │ └── index.ts
88
+ ├── auth/
89
+ │ ├── constants.ts # Auth-specific constants
90
+ │ ├── validateToken.ts
91
+ │ └── index.ts
92
+ ```
93
+
94
+ (Do) Import constants directly from the owning domain's `constants.ts`:
95
+ ```typescript
96
+ // features/pricing/calculatePrice.ts
97
+ import { TAX_RATE, DEFAULT_CURRENCY } from "./constants.js";
98
+ ```
99
+
100
+ (Do) Keep all exports in `constants.ts` as named exports with the same shape (plain values):
101
+ ```typescript
102
+ // constants.ts
103
+ export const RECONNECT_INTERVAL_MS = 5000;
104
+ export const MAX_RETRIES = 3;
105
+ export const DEFAULT_TIMEOUT_MS = 30_000;
106
+ ```
107
+
108
+ (Do) Keep single-use constants inline in the file that uses them:
109
+ ```typescript
110
+ // features/pricing/applyDiscount.ts
111
+ const MAX_DISCOUNT_PERCENT = 50;
112
+
113
+ export default function applyDiscount(price: number, rate: number): number {
114
+ const capped = Math.min(rate, MAX_DISCOUNT_PERCENT / 100);
115
+ return price * (1 - capped);
116
+ }
117
+ ```
118
+
119
+ ### Don't
120
+
121
+ (Don't) Use a default export in a constants file:
122
+ ```typescript
123
+ // Bad: default export forces consumers to name the import
124
+ export default {
125
+ DEFAULT_CURRENCY: "USD",
126
+ TAX_RATE: 0.21,
127
+ };
128
+ ```
129
+
130
+ (Don't) Scatter constants across unrelated files instead of collecting them in `constants.ts`:
131
+ ```typescript
132
+ // Bad: constants buried inside implementation files
133
+ // calculatePrice.ts
134
+ export const TAX_RATE = 0.21;
135
+ const calculatePrice = (base: number) => base * (1 + TAX_RATE);
136
+ export default calculatePrice;
137
+ ```
138
+
139
+ (Don't) Place constants far from the domain that owns them:
140
+ ```typescript
141
+ // Bad: pricing constants in a top-level shared file
142
+ // src/constants.ts
143
+ export const TAX_RATE = 0.21; // Belongs in pricing/
144
+ export const MAX_RETRIES = 3; // Belongs in network/
145
+ export const SESSION_TTL_MS = 3600; // Belongs in auth/
146
+ ```
147
+
148
+ (Don't) Mix constants with functions, types, or other non-constant exports:
149
+ ```typescript
150
+ // Bad: constants.ts should only contain constant values
151
+ export const MAX_RETRIES = 3;
152
+ export const formatRetryMessage = (n: number) => `Retry ${n} of ${MAX_RETRIES}`;
153
+ export type RetryConfig = { max: number };
154
+ ```
155
+
156
+ ---
157
+
158
+ ## code/export/barrel-public-api
159
+
160
+ Barrel files must exist only to define a public API surface. Internal implementation code must import concrete modules directly rather than routing through local barrels. Public barrels may exist at package entry points or at explicitly public subdomain boundaries.
161
+
162
+ ### Do
163
+
164
+ (Do) Use a barrel for a package entry point or explicit public API:
165
+ ```typescript
166
+ // src/index.ts
167
+ export { Button } from "./components/Button.js";
168
+ export type { ButtonProps } from "./components/Button.types.js";
169
+ ```
170
+
171
+ (Do) Import internal implementation code from the owning file:
172
+ ```typescript
173
+ // src/build/buildTheme.ts
174
+ import { computeDeltas } from "./computeDeltas.js";
175
+ import { recoverPrimitiveRef } from "./recoverPrimitiveRef.js";
176
+ ```
177
+
178
+ (Do) Keep compatibility barrels thin and documented as public facades:
179
+ ```typescript
180
+ /** Public compatibility surface. */
181
+ export { computeDeltas } from "./computeDeltas.js";
182
+ export { formatDelta } from "./formatDelta.js";
183
+ ```
184
+
185
+ ### Don't
186
+
187
+ (Don't) Route internal code through a sibling barrel just because it exists:
188
+ ```typescript
189
+ // Bad: internal implementation depends on a local barrel
190
+ import { computeDeltas, recoverPrimitiveRef } from "./index.js";
191
+ ```
192
+
193
+ (Don't) Hide domain ownership behind convenience barrels:
194
+ ```typescript
195
+ // Bad: types appear to belong to context.ts instead of their owning domains
196
+ import type { Artifact, CSSNode, OverlayToken } from "./context.js";
197
+ ```
198
+
199
+ (Don't) Create folder barrels for implementation-only directories with no public API contract:
200
+ ```typescript
201
+ // build/helpers/index.ts
202
+ export * from "./parse.js";
203
+ export * from "./format.js";
204
+ export * from "./walk.js";
205
+ ```
206
+
207
+ ---
208
+
209
+ ## code/export/shape
210
+
211
+ Files must use either a single default export or multiple named exports. When using multiple named exports, all exports must have the same type or shape.
212
+
213
+ ### Do
214
+
215
+ (Do) Use a single default export for files implementing a single component or function:
216
+ ```typescript
217
+ // ComponentName.tsx
218
+ export default ComponentName;
219
+ ```
220
+
221
+ (Do) Name atomic function files after the function they export:
222
+ ```typescript
223
+ // assignElement.ts
224
+ export default function assignElement(target: Element, source: Partial<Element>) {
225
+ // ...
226
+ }
227
+
228
+ // formatCurrency.ts
229
+ export default function formatCurrency(value: number) {
230
+ // ...
231
+ }
232
+ ```
233
+
234
+ (Do) Use multiple named exports for files providing a public API or a collection of related items, and ensure all exports have the same type:
235
+ ```typescript
236
+ // index.ts
237
+ export { ComponentA, ComponentB };
238
+
239
+ // types.ts
240
+ export type TypeA = { ... };
241
+ export type TypeB = { ... };
242
+
243
+ // Consistent export shape:
244
+ export const myFuncA = (value: string) => {};
245
+ export const myFuncB = (value: string) => {};
246
+ export const myFuncC = (value: string) => {};
247
+ ```
248
+
249
+ ### Don't
250
+
251
+ (Don't) Mix default and unrelated named exports in a way that confuses the file's purpose:
252
+ ```typescript
253
+ export default ComponentName;
254
+ export const helper = () => {};
255
+ ```
256
+
257
+ (Don't) Name an atomic function file with a name that doesn't match its exported function:
258
+ ```typescript
259
+ // helpers.ts — wrong: generic name instead of function name
260
+ export default function assignElement(target: Element, source: Partial<Element>) {
261
+ // ...
262
+ }
263
+
264
+ // utils.ts — wrong: generic name instead of function name
265
+ export default function formatCurrency(value: number) {
266
+ // ...
267
+ }
268
+ ```
269
+
270
+ (Don't) Provide multiple unrelated exports from a file meant for a single domain:
271
+ ```typescript
272
+ export default debounce;
273
+ export const throttle = () => {};
274
+ export const logger = () => {};
275
+ ```
276
+
277
+ (Don't) Export objects of different types or shapes from the same file:
278
+ ```typescript
279
+ export const transformer = (value: string) => {};
280
+ export const reducer = (map: string[]) => {};
281
+ class ABC {}
282
+ export { ABC };
283
+ ```
284
+
285
+ ---
286
+
287
+ ## code/function/composition
288
+
289
+ Each function must handle exactly one specific task. Complex operations must be broken down into smaller, single-purpose functions.
290
+
291
+ ### Do
292
+
293
+ (Do) Define functions with a single, clear responsibility.
294
+ ```typescript
295
+ const validateEmail = (email: string): boolean => {
296
+ return /^[^@]+@[^@]+\\.[^@]+$/.test(email);
297
+ };
298
+ ```
299
+
300
+ (Do) Compose single-purpose functions to build complex logic.
301
+ ```typescript
302
+ const validatePassword = (password: string): boolean => {
303
+ return password.length >= 8;
304
+ };
305
+
306
+ const validateForm = (email: string, password:string): boolean => {
307
+ return validateEmail(email) && validatePassword(password);
308
+ };
309
+ ```
310
+
311
+ ### Don't
312
+
313
+ (Don't) Mix multiple responsibilities in a single function.
314
+ ```typescript
315
+ const processUserData = (user: any) => {
316
+ // Validates user data
317
+ // Transforms the data
318
+ // Saves to the database
319
+ // Sends a notification
320
+ };
321
+ ```
322
+
323
+ ---
324
+
325
+ ## code/function/location
326
+
327
+ Functions must be defined as close to their broadest point of use as possible. Shared utilities must be placed in a common location, and each function must serve a single feature or responsibility.
328
+
329
+ ### Do
330
+
331
+ (Do) Place functions in the file or module where they are most broadly used. If a function is shared across multiple components or modules, place it in a common utility location.
332
+ ```typescript
333
+ // utils/math.ts
334
+ export function calculateSum(a: number, b: number): number {
335
+ return a + b;
336
+ }
337
+
338
+ // Used in multiple places
339
+ import { calculateSum } from './utils/math.js';
340
+
341
+ // For single-use functions, keep them close to their usage:
342
+ ```typescript
343
+ // services/UserService.ts
344
+ const formatUserName = (user: User) => `${user.firstName} ${user.lastName}`;
345
+
346
+ const getProfileData = (user: User) => {
347
+ const formattedName = formatUserName(user);
348
+ return {
349
+ ...user,
350
+ formattedName,
351
+ };
352
+ };
353
+
354
+ ### Don't
355
+
356
+ (Don't) Place functions far from where they are used, or in a generic location if only used in one place.
357
+ ```typescript
358
+ // utils/validators.ts
359
+ export const validateUser = (user: User) => { ... }; // Only used in one component
360
+
361
+ // services/UserService.ts
362
+ import { validateUser } from '../utils/validators.js'; // Less ideal if only used here
363
+
364
+ // Don't mix unrelated functions in a single file:
365
+ ```typescript
366
+ export const mathFunc = () => {};
367
+ export const stringFunc = () => {};
368
+
369
+ ---
370
+
371
+ ## code/function/purity
372
+
373
+ Functions should be pure where possible. A pure function returns the same output for the same input and does not modify external state or perform I/O operations. If a function must perform side effects (e.g., I/O in CLI tools), it must be explicitly annotated and documented with the reason for impurity using the @note tag in TSDoc/JSDoc.
374
+
375
+ ### Do
376
+
377
+ (Do) Create pure functions that rely only on their inputs to compute the output.
378
+ ```typescript
379
+ const calculatePrice = (basePrice: number, taxRate: number): number => {
380
+ return basePrice * (1 + taxRate);
381
+ };
382
+ ```
383
+
384
+ (Do) Annotate and document impure functions that perform I/O or modify external state, explaining why impurity is necessary using @note.
385
+ ```typescript
386
+ /**
387
+ * Writes data to the file system.
388
+ * @note - This function is impure - it modifies the file system.
389
+ */
390
+ function writeOutputToFile(data: string, path: string) {
391
+ fs.writeFileSync(path, data);
392
+ }
393
+ ```
394
+
395
+ ### Don't
396
+
397
+ (Don't) Create impure functions without annotation or documentation.
398
+ ```typescript
399
+ let total = 0;
400
+ const addToTotal = (value: number) => {
401
+ total += value; // Modifies external state, not documented
402
+ };
403
+ ```
404
+
405
+ (Don't) Hide side effects in functions that appear pure.
406
+ ```typescript
407
+ function getConfig() {
408
+ // Reads from disk without annotation
409
+ return fs.readFileSync('config.json');
410
+ }
411
+ ```
412
+
413
+ ---
414
+
415
+ ## code/naming/function-verb
416
+
417
+ All function and method names must start with a verb that describes the action the function performs. Names like `pathToX` or `dataForUser` describe a result, not an action — use `findPathToX` or `fetchDataForUser` instead. This makes the function's intent immediately clear and distinguishes functions from plain values or constants.
418
+
419
+ ### Do
420
+
421
+ (Do) Start function names with an action verb:
422
+ ```typescript
423
+ const findPathToModule = (name: string): string => { /* ... */ };
424
+ const fetchDataForUser = (userId: string): Promise<User> => { /* ... */ };
425
+ const parseConfigFile = (path: string): Config => { /* ... */ };
426
+ const calculateTotalPrice = (items: Item[]): number => { /* ... */ };
427
+ const isValidEmail = (email: string): boolean => { /* ... */ };
428
+ const hasPermission = (user: User, action: string): boolean => { /* ... */ };
429
+ ```
430
+
431
+ (Do) Use descriptive verb prefixes that convey the operation:
432
+ ```typescript
433
+ // Retrieval: get, fetch, find, resolve, load, read
434
+ const getUser = (id: string): User => { /* ... */ };
435
+ const fetchOrders = (): Promise<Order[]> => { /* ... */ };
436
+ const findMatchingRule = (rules: Rule[], input: string): Rule | undefined => { /* ... */ };
437
+
438
+ // Transformation: format, parse, convert, transform, map, normalize
439
+ const formatDate = (date: Date): string => { /* ... */ };
440
+ const parseResponse = (raw: string): Response => { /* ... */ };
441
+
442
+ // Validation: is, has, can, should, validate, check
443
+ const isActive = (user: User): boolean => { /* ... */ };
444
+ const hasExpired = (token: Token): boolean => { /* ... */ };
445
+ const validateInput = (data: unknown): data is FormData => { /* ... */ };
446
+
447
+ // Mutation: set, update, add, remove, delete, reset, clear
448
+ const setTheme = (theme: Theme): void => { /* ... */ };
449
+ const removeItem = (id: string): void => { /* ... */ };
450
+
451
+ // Creation: create, build, generate, compose, make
452
+ const createConnection = (config: Config): Connection => { /* ... */ };
453
+ const buildQuery = (params: Params): string => { /* ... */ };
454
+ ```
455
+
456
+ ### Don't
457
+
458
+ (Don't) Name functions as nouns or noun phrases — they read like values, not actions:
459
+ ```typescript
460
+ // Bad: reads like a variable, not a function
461
+ const pathToModule = (name: string): string => { /* ... */ };
462
+ // Good: findPathToModule
463
+
464
+ const dataForUser = (userId: string): Promise<User> => { /* ... */ };
465
+ // Good: fetchDataForUser
466
+
467
+ const userPermissions = (user: User): Permission[] => { /* ... */ };
468
+ // Good: getPermissions or listPermissions
469
+ ```
470
+
471
+ (Don't) Use vague or non-descriptive verbs that don't convey meaning:
472
+ ```typescript
473
+ // Bad: "do" and "process" are too vague on their own
474
+ const doEmail = (email: string) => { /* ... */ };
475
+ // Good: sendEmail, validateEmail, formatEmail
476
+
477
+ const processUser = (user: User) => { /* ... */ };
478
+ // Good: activateUser, deactivateUser, updateUser
479
+ ```
480
+
481
+ ---
482
+
483
+ ## code/naming/single-export-file
484
+
485
+ Files that contain a single export must be named after that export. This applies to functions, classes, types, constants, and any other single-export module. The file name must match the exported identifier exactly, preserving its casing (camelCase for functions/variables, PascalCase for classes/types/components).
486
+
487
+ ### Do
488
+
489
+ (Do) Name files after the single function they export:
490
+ ```typescript
491
+ // isAccepted.ts
492
+ export const isAccepted = (status: string): boolean => {
493
+ return status === "accepted";
494
+ };
495
+
496
+ // formatCurrency.ts
497
+ export default function formatCurrency(value: number): string {
498
+ return `$${value.toFixed(2)}`;
499
+ }
500
+ ```
501
+
502
+ (Do) Name files after the single type or interface they export:
503
+ ```typescript
504
+ // ConnectionConfig.ts
505
+ export interface ConnectionConfig {
506
+ host: string;
507
+ port: number;
508
+ }
509
+
510
+ // UserRole.ts
511
+ export type UserRole = "admin" | "editor" | "viewer";
512
+ ```
513
+
514
+ (Do) Name files after the single class they export:
515
+ ```typescript
516
+ // EventEmitter.ts
517
+ export class EventEmitter {
518
+ // ...
519
+ }
520
+ ```
521
+
522
+ (Do) Name files after the single constant they export:
523
+ ```typescript
524
+ // DEFAULT_TIMEOUT.ts
525
+ export const DEFAULT_TIMEOUT = 5000;
526
+ ```
527
+
528
+ ### Don't
529
+
530
+ (Don't) Use generic names like `helpers.ts`, `utils.ts`, or `types.ts` for files with a single export:
531
+ ```typescript
532
+ // helpers.ts — wrong: should be isAccepted.ts
533
+ export const isAccepted = (status: string): boolean => {
534
+ return status === "accepted";
535
+ };
536
+
537
+ // utils.ts — wrong: should be formatCurrency.ts
538
+ export default function formatCurrency(value: number): string {
539
+ return `$${value.toFixed(2)}`;
540
+ }
541
+ ```
542
+
543
+ (Don't) Use names that describe the domain instead of the export:
544
+ ```typescript
545
+ // validation.ts — wrong: should be isAccepted.ts
546
+ export const isAccepted = (status: string): boolean => {
547
+ return status === "accepted";
548
+ };
549
+
550
+ // network.ts — wrong: should be ConnectionConfig.ts
551
+ export interface ConnectionConfig {
552
+ host: string;
553
+ port: number;
554
+ }
555
+ ```
556
+
557
+ ---
558
+
559
+ ## code/package/lib-folder
560
+
561
+ Packages that export reusable logic (components, utilities, hooks, types) must organize their exportable code in a `lib` folder within the `src` directory. This convention ensures consistency across the monorepo and clearly identifies code that is part of the package's public API.
562
+
563
+ ### Do
564
+
565
+ (Do) Place all reusable/exportable code in `src/lib/`:
566
+ ```
567
+ packages/my-package/
568
+ ├── src/
569
+ │ ├── lib/
570
+ │ │ ├── Button/
571
+ │ │ │ ├── Button.tsx
572
+ │ │ │ ├── Button.tests.tsx
573
+ │ │ │ ├── types.ts
574
+ │ │ │ └── index.ts
575
+ │ │ ├── hooks/
576
+ │ │ │ └── useToggle.ts
577
+ │ │ ├── types/
578
+ │ │ │ └── index.ts
579
+ │ │ └── index.ts
580
+ │ ├── storybook/ # Storybook-specific files (not exported)
581
+ │ └── index.ts # Re-exports from lib
582
+ └── package.json
583
+ ```
584
+
585
+ (Do) Re-export from the lib folder in the package entry point:
586
+ ```typescript
587
+ // src/index.ts
588
+ export * from "./lib/index.js";
589
+ ```
590
+
591
+ (Do) Use the lib folder for components, hooks, utilities, types, and any other code meant to be consumed by package users:
592
+ ```typescript
593
+ // src/lib/index.ts
594
+ export * from "./Button/index.js";
595
+ export * from "./hooks/index.js";
596
+ export type * from "./types/index.js";
597
+ ```
598
+
599
+ ### Don't
600
+
601
+ (Don't) Use alternative folder names like `ui`, `components`, or `utils` for exportable code at the package level:
602
+ ```
603
+ // Bad: Using 'ui' instead of 'lib'
604
+ packages/my-package/
605
+ ├── src/
606
+ │ ├── ui/ # Wrong: should be 'lib'
607
+ │ │ └── Button/
608
+ │ └── index.ts
609
+ ```
610
+
611
+ (Don't) Mix exportable and non-exportable code at the same level:
612
+ ```
613
+ // Bad: Mixing concerns
614
+ packages/my-package/
615
+ ├── src/
616
+ │ ├── Button/ # Component mixed with...
617
+ │ ├── storybook/ # ...non-exportable storybook config
618
+ │ └── test-utils/ # ...and test utilities
619
+ ```
620
+
621
+ (Don't) Create deeply nested lib-like structures; keep lib at the top level of src:
622
+ ```
623
+ // Bad: Nested lib folders
624
+ packages/my-package/
625
+ ├── src/
626
+ │ ├── features/
627
+ │ │ └── lib/ # Wrong: lib should be at src level
628
+ ```
629
+
630
+ ---
631
+
632
+ ## code/types/file
633
+
634
+ Reusable domain types must be collected in a `types.ts` file using named exports (no default export), colocated at the level of the domain that owns them. Types that are only used in a single file may be declared inline in that file instead of being extracted to `types.ts`.
635
+
636
+ ### Do
637
+
638
+ (Do) Create a `types.ts` file with named exports for types shared across the domain:
639
+ ```typescript
640
+ // features/pricing/types.ts
641
+ export type Currency = "USD" | "EUR" | "GBP";
642
+ export interface PriceBreakdown {
643
+ base: number;
644
+ tax: number;
645
+ total: number;
646
+ }
647
+ export type DiscountStrategy = "percentage" | "fixed";
648
+ ```
649
+
650
+ (Do) Colocate `types.ts` inside the domain folder it belongs to:
651
+ ```
652
+ features/
653
+ ├── pricing/
654
+ │ ├── types.ts # Shared domain types live here
655
+ │ ├── constants.ts
656
+ │ ├── calculatePrice.ts
657
+ │ └── index.ts
658
+ ├── auth/
659
+ │ ├── types.ts # Auth-specific types
660
+ │ ├── validateToken.ts
661
+ │ └── index.ts
662
+ ```
663
+
664
+ (Do) Keep single-use types inline in the file that uses them:
665
+ ```typescript
666
+ // features/pricing/applyDiscount.ts
667
+ type DiscountResult = { discounted: number; savings: number };
668
+
669
+ export default function applyDiscount(price: number, rate: number): DiscountResult {
670
+ const savings = price * rate;
671
+ return { discounted: price - savings, savings };
672
+ }
673
+ ```
674
+
675
+ (Do) Import shared types from the owning domain's `types.ts`:
676
+ ```typescript
677
+ // features/pricing/calculatePrice.ts
678
+ import type { Currency, PriceBreakdown } from "./types.js";
679
+ ```
680
+
681
+ ### Don't
682
+
683
+ (Don't) Use a default export in a types file:
684
+ ```typescript
685
+ // Bad: default export for a collection of types
686
+ export default interface PriceBreakdown {
687
+ base: number;
688
+ tax: number;
689
+ total: number;
690
+ }
691
+ ```
692
+
693
+ (Don't) Extract single-use types to `types.ts` when they are only used in one file:
694
+ ```typescript
695
+ // Bad: types.ts contains a type only used in applyDiscount.ts
696
+ // types.ts
697
+ export type DiscountResult = { discounted: number; savings: number };
698
+
699
+ // applyDiscount.ts
700
+ import type { DiscountResult } from "./types.js"; // Unnecessary indirection
701
+ ```
702
+
703
+ (Don't) Place types far from the domain that owns them:
704
+ ```typescript
705
+ // Bad: all types dumped in a top-level shared file
706
+ // src/types.ts
707
+ export type Currency = "USD" | "EUR" | "GBP"; // Belongs in pricing/
708
+ export type AuthToken = { jwt: string }; // Belongs in auth/
709
+ export type RetryPolicy = { max: number }; // Belongs in network/
710
+ ```
711
+
712
+ (Don't) Mix types with functions, constants, or other non-type exports:
713
+ ```typescript
714
+ // Bad: types.ts should only contain type declarations
715
+ export type Currency = "USD" | "EUR" | "GBP";
716
+ export const DEFAULT_CURRENCY: Currency = "USD";
717
+ export const formatCurrency = (value: number, currency: Currency) => `${currency} ${value}`;
718
+ ```
719
+
720
+ ---