From c66c14a211d8f7dcc7c1d33e9c1b6dafb83f7e56 Mon Sep 17 00:00:00 2001 From: DrJKL Date: Sun, 11 Jan 2026 02:39:35 -0800 Subject: [PATCH] docs: add type safety rules for TypeScript files --- AGENTS.md | 1 + docs/typescript/type-safety.md | 186 +++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 docs/typescript/type-safety.md diff --git a/AGENTS.md b/AGENTS.md index 743572be3c..a60cb37892 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -200,6 +200,7 @@ See @docs/testing/*.md for detailed patterns. ## External Resources - Vue: +- @docs/typescript/type-safety.md - Tailwind: - VueUse: - shadcn/vue: diff --git a/docs/typescript/type-safety.md b/docs/typescript/type-safety.md new file mode 100644 index 0000000000..a557c7e734 --- /dev/null +++ b/docs/typescript/type-safety.md @@ -0,0 +1,186 @@ +--- +globs: + - '**/*.ts' + - '**/*.tsx' + - '**/*.vue' +--- + +# Type Safety Rules + +## Never Use Type Assertions (`as`) + +Type assertions bypass TypeScript's type checking. Instead, use proper type guards and narrowing. + +### DOM Elements + +❌ **Wrong:** +```typescript +const el = e.target as HTMLInputElement +el.value +``` + +✅ **Correct:** +```typescript +if (e.target instanceof HTMLInputElement) { + e.target.value +} +``` + +### Optional Properties on Objects + +❌ **Wrong:** +```typescript +const obj = value as { prop?: string } +if (obj.prop) { ... } +``` + +✅ **Correct:** +```typescript +if ('prop' in value && typeof value.prop === 'string') { + value.prop +} +``` + +### Constructor/Static Properties + +❌ **Wrong:** +```typescript +const ctor = node.constructor as { type?: string } +const type = ctor.type +``` + +✅ **Correct:** +```typescript +const ctor = node.constructor +const type = 'type' in ctor && typeof ctor.type === 'string' ? ctor.type : undefined +``` + +## Fix the Source Type, Don't Cast + +When you find yourself needing a cast, ask: "Can I fix the type definition instead?" + +### Missing Interface Properties + +If a property exists at runtime but not in the type, add it to the interface: + +❌ **Wrong:** +```typescript +const box = this.search_box as { close?: () => void } +box?.close?.() +``` + +✅ **Correct:** +```typescript +// Update the type definition +search_box?: HTMLDivElement & ICloseable + +// Then use directly +this.search_box?.close() +``` + +### Callback Parameter Types + +If a callback receives a different type than declared, fix the callback signature: + +❌ **Wrong:** +```typescript +onDrawTooltip?: (link: LLink) => void +// Later... +onDrawTooltip(link as LLink) // link is actually LinkSegment +``` + +✅ **Correct:** +```typescript +onDrawTooltip?: (link: LinkSegment) => void +// Later... +onDrawTooltip(link) // No cast needed +``` + +## Create Type Guard Functions + +For repeated type checks, create reusable type guards: + +```typescript +interface IPanel extends Element, ICloseable { + node?: LGraphNode + graph?: LGraph +} + +function isPanel(el: Element): el is IPanel { + return 'close' in el && typeof el.close === 'function' +} + +// Usage +for (const panel of panels) { + if (!isPanel(panel)) continue + panel.close() // TypeScript knows panel is IPanel +} +``` + +## When Casts Are Unavoidable + +Some casts are genuinely unavoidable. Document why: + +```typescript +/** + * @deprecated Prefer {@link structuredClone} + * Note: JSON.parse returns `unknown`, so type assertions are unavoidable here. + * This function is deprecated precisely because it cannot be made type-safe. + */ +cloneObject(obj: T): T { + const cloned: unknown = JSON.parse(JSON.stringify(obj)) + return cloned as T // Unavoidable - JSON.parse returns unknown +} +``` + +### Acceptable Cast Scenarios + +1. **`JSON.parse` results** - Returns `unknown`, must be cast +2. **Conditional types in return positions** - TypeScript can't always narrow these +3. **Third-party library limitations** - When you can't fix the source types + +## Prefer Specific Over General + +Use the most specific type possible: + +❌ **Wrong:** +```typescript +const value = obj as any +const value = obj as unknown +const value = obj as Record +``` + +✅ **Correct:** +```typescript +// Use the actual expected type +const value: ISerialisedNode = obj +// Or use type guards to narrow +if (isSerialisedNode(obj)) { ... } +``` + +## Union Types and Narrowing + +For union types, use proper narrowing instead of casting: + +❌ **Wrong:** +```typescript +// NodeId = number | string +(node.id as number) - (other.id as number) +``` + +✅ **Correct:** +```typescript +if (typeof node.id === 'number' && typeof other.id === 'number') { + node.id - other.id +} +``` + +## Summary + +1. **Never use `as`** unless absolutely unavoidable +2. **Use `instanceof`** for class/DOM element checks +3. **Use `'prop' in obj`** for property existence checks +4. **Use `typeof`** for primitive type checks +5. **Fix source types** instead of casting at usage sites +6. **Create type guards** for repeated patterns +7. **Document unavoidable casts** with clear explanations