Skip to content

Commit 1bf4b19

Browse files
committed
docs(conventions): generalize TypeScript examples
1 parent 619d849 commit 1bf4b19

File tree

1 file changed

+69
-61
lines changed

1 file changed

+69
-61
lines changed

.cursor/rules/typescript-conventions.mdc

Lines changed: 69 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ Always use inline type imports for type-only imports:
1414

1515
```ts
1616
// ✅ Good
17-
import { type RuleModule, ESLintUtils } from "@typescript-eslint/utils"
17+
import { type User, fetchUser } from "./api"
1818

1919
// ❌ Bad - separate import statement
20-
import type { RuleModule } from "@typescript-eslint/utils"
21-
import { ESLintUtils } from "@typescript-eslint/utils"
20+
import type { User } from "./api"
21+
import { fetchUser } from "./api"
2222
```
2323

2424
### Import Order
@@ -30,9 +30,9 @@ import { ESLintUtils } from "@typescript-eslint/utils"
3030
```ts
3131
import path from "node:path"
3232

33-
import { ESLintUtils } from "@typescript-eslint/utils"
33+
import { z } from "zod"
3434

35-
import { createRule } from "../utils/create-rule.js"
35+
import { validateInput } from "../utils/validation.js"
3636
```
3737

3838
## Type Definitions
@@ -43,15 +43,17 @@ Prefer `interface` for object shapes:
4343

4444
```ts
4545
// ✅ Good
46-
interface RuleOptions {
47-
allowedCallees: string[]
48-
maxDepth: number
46+
interface UserProfile {
47+
name: string
48+
email: string
49+
age: number
4950
}
5051

5152
// ❌ Bad
52-
type RuleOptions = {
53-
allowedCallees: string[]
54-
maxDepth: number
53+
type UserProfile = {
54+
name: string
55+
email: string
56+
age: number
5557
}
5658
```
5759

@@ -61,28 +63,32 @@ Prefer simple union string types over enums or const object patterns:
6163

6264
```ts
6365
// ✅ Good - simple union type
64-
type MessageId = "complexExpression" | "nestedMacro"
66+
type Status = "idle" | "loading" | "success" | "error"
6567

6668
// ❌ Bad - const object (fake enum pattern)
67-
const MessageId = {
68-
ComplexExpression: "complexExpression",
69-
NestedMacro: "nestedMacro",
69+
const Status = {
70+
Idle: "idle",
71+
Loading: "loading",
72+
Success: "success",
73+
Error: "error",
7074
} as const
71-
type MessageId = (typeof MessageId)[keyof typeof MessageId]
75+
type Status = (typeof Status)[keyof typeof Status]
7276

7377
// ❌ Bad - enum
74-
enum MessageId {
75-
ComplexExpression = "complexExpression",
76-
NestedMacro = "nestedMacro",
78+
enum Status {
79+
Idle = "idle",
80+
Loading = "loading",
81+
Success = "success",
82+
Error = "error",
7783
}
7884
```
7985

8086
When you need both runtime values and types, extract the union from an array:
8187

8288
```ts
8389
// ✅ Good - array as source of truth
84-
const LINGUI_MACROS = ["t", "Trans", "msg", "defineMessage"] as const
85-
type LinguiMacro = (typeof LINGUI_MACROS)[number]
90+
const SUPPORTED_FORMATS = ["json", "yaml", "toml"] as const
91+
type SupportedFormat = (typeof SUPPORTED_FORMATS)[number]
8692
```
8793

8894
## Functions
@@ -93,22 +99,22 @@ Use function declarations for pure functions, utility functions, and React compo
9399

94100
```ts
95101
// ✅ Good - pure function
96-
function isLinguiMacro(node: TSESTree.Node): boolean {
97-
return node.type === "TaggedTemplateExpression"
102+
function formatCurrency(amount: number, currency: string): string {
103+
return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount)
98104
}
99105

100106
// ✅ Good - React component
101-
function UserProfile({ user }: UserProfileProps): React.ReactElement {
107+
function UserCard({ user }: UserCardProps): React.ReactElement {
102108
return <div>{user.name}</div>
103109
}
104110

105111
// ❌ Bad - arrow function for pure function
106-
const isLinguiMacro = (node: TSESTree.Node): boolean => {
107-
return node.type === "TaggedTemplateExpression"
112+
const formatCurrency = (amount: number, currency: string): string => {
113+
return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount)
108114
}
109115

110116
// ❌ Bad - arrow function for component
111-
const UserProfile = ({ user }: UserProfileProps) => {
117+
const UserCard = ({ user }: UserCardProps) => {
112118
return <div>{user.name}</div>
113119
}
114120
```
@@ -118,14 +124,14 @@ const UserProfile = ({ user }: UserProfileProps) => {
118124
Use arrow functions for inline callbacks and closures:
119125

120126
```ts
121-
// ✅ Good
122-
const listeners = nodes.filter((node) => isRelevant(node))
127+
// ✅ Good - inline callback
128+
const activeUsers = users.filter((user) => user.isActive)
123129

124130
// ✅ Good - closure capturing context
125-
function createVisitor(context: RuleContext) {
131+
function createHandler(config: Config) {
126132
return {
127-
CallExpression: (node) => {
128-
// Uses context from closure
133+
onSubmit: (data) => {
134+
// Uses config from closure
129135
},
130136
}
131137
}
@@ -137,14 +143,12 @@ All exported functions must have explicit return types:
137143

138144
```ts
139145
// ✅ Good
140-
export function createRule<TOptions extends unknown[]>(
141-
options: RuleCreatorOptions<TOptions>
142-
): RuleModule<string, TOptions> {
146+
export function parseConfig(input: string): Config {
143147
// ...
144148
}
145149

146150
// ❌ Bad - missing return type
147-
export function createRule(options) {
151+
export function parseConfig(input) {
148152
// ...
149153
}
150154
```
@@ -154,14 +158,14 @@ export function createRule(options) {
154158
Use type guards for narrowing:
155159

156160
```ts
157-
function isCallExpression(node: TSESTree.Node): node is TSESTree.CallExpression {
158-
return node.type === "CallExpression"
161+
function isError(value: unknown): value is Error {
162+
return value instanceof Error
159163
}
160164

161165
// Usage
162-
if (isCallExpression(node)) {
163-
// node is typed as CallExpression here
164-
console.log(node.callee)
166+
if (isError(result)) {
167+
// result is typed as Error here
168+
console.log(result.message)
165169
}
166170
```
167171

@@ -170,22 +174,24 @@ if (isCallExpression(node)) {
170174
### Variables
171175

172176
- **Boolean**: Use auxiliary verbs: `isLoading`, `hasError`, `shouldUpdate`
173-
- **Collections**: Use plural: `items`, `rules`, `visitors`
174-
- **Functions**: Use verbs: `createRule`, `isValid`, `getType`
177+
- **Collections**: Use plural: `items`, `users`, `options`
178+
- **Functions**: Use verbs: `createUser`, `isValid`, `getConfig`
175179

176180
### Files
177181

178-
- **Rules**: `rule-name.ts` (kebab-case)
179-
- **Utils**: `createRule.ts` (camelCase)
180-
- **Types**: `types.ts` or inline
182+
- **Components**: `UserCard.tsx` (PascalCase)
183+
- **Utilities**: `formatCurrency.ts` (camelCase)
184+
- **Constants/Config**: `constants.ts`, `config.ts` (camelCase)
185+
- **Types**: `types.ts` or co-located
181186

182187
### Constants
183188

184189
Use SCREAMING_SNAKE_CASE for true constants:
185190

186191
```ts
187-
const LINGUI_MACROS = ["t", "Trans", "msg", "defineMessage"] as const
188-
const DEFAULT_MAX_DEPTH = 1
192+
const SUPPORTED_FORMATS = ["json", "yaml", "toml"] as const
193+
const DEFAULT_TIMEOUT_MS = 5000
194+
const API_BASE_URL = "https://api.example.com"
189195
```
190196

191197
## Nullish Handling
@@ -204,10 +210,10 @@ const value = input || defaultValue
204210

205211
```ts
206212
// ✅ Good
207-
const name = user?.profile?.name
213+
const city = user?.address?.city
208214

209215
// ❌ Bad
210-
const name = user && user.profile && user.profile.name
216+
const city = user && user.address && user.address.city
211217
```
212218

213219
## Comments
@@ -219,31 +225,33 @@ Use JSDoc without type annotations - let TypeScript handle the types:
219225
```ts
220226
// ✅ Good - TypeScript style (no type annotations in JSDoc)
221227
/**
222-
* Creates an ESLint rule for Lingui macro validation.
228+
* Formats a date relative to now (e.g., "2 hours ago").
223229
*
224-
* @param options - Rule configuration options
225-
* @returns Configured ESLint rule module
230+
* @param date - The date to format
231+
* @param locale - Optional locale for formatting
232+
* @returns Human-readable relative time string
226233
*/
227-
export function createLinguiRule(options: RuleOptions): RuleModule {
234+
export function formatRelativeTime(date: Date, locale?: string): string {
228235
// ...
229236
}
230237

231238
// ❌ Bad - JavaScript style (redundant type annotations)
232239
/**
233-
* Creates an ESLint rule for Lingui macro validation.
240+
* Formats a date relative to now.
234241
*
235-
* @param {RuleOptions} options - Rule configuration options
236-
* @returns {RuleModule} Configured ESLint rule module
242+
* @param {Date} date - The date to format
243+
* @param {string} [locale] - Optional locale
244+
* @returns {string} Relative time string
237245
*/
238-
export function createLinguiRule(options: RuleOptions): RuleModule {
246+
export function formatRelativeTime(date: Date, locale?: string): string {
239247
// ...
240248
}
241249
```
242250

243251
### Inline Comments for Non-Obvious Logic
244252

245253
```ts
246-
// TypeScript's getTypeAtLocation returns the *widened* type for literals,
247-
// so we need to check the contextual type instead
248-
const contextualType = typeChecker.getContextualType(node)
254+
// Intl.RelativeTimeFormat doesn't auto-select units,
255+
// so we need to determine the appropriate unit ourselves
256+
const units = selectTimeUnit(diffMs)
249257
```

0 commit comments

Comments
 (0)