React Types
ComponentProps
import { ComponentProps } from 'react'
// Get all props from a native button element
type ButtonProps = ComponentProps<'button'>
const MyButton = (props: ButtonProps) => {
return <button {...props} />
}
// Or extract specific props from an HTML element
type InputProps = ComponentProps<'input'>['type']
// ^? "text" | "password" | "checkbox" | "radio" | etc...PropsWithChildren
import { PropsWithChildren } from 'react'
export function Axemple({
children,
href,
}: PropsWithChildren<{ href: string }>) {
return <a href={href}>{children}</a>
}TypeScript
type ValuesOf<T> = T[keyof T]
type XXType = ValuesOf<typeof XX>
type NonNullable<T>// Without & XOR
// source: https://github.com/Microsoft/TypeScript/issues/14094#issuecomment-373782604
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
type XOR<T, U> = T | U extends Record<string, unknown>
? (Without<T, U> & U) | (Without<U, T> & T)
: T | U/**
- Excludes common falsy values from a type
- @example
- type Example = string | number | null | undefined | 0 | '' | false
- type ValidExample = ValidValue<Example>
- ^? string | number
**/
export type ValidValue<T> = Exclude<T, null | undefined | 0 | '' | false>
export type Optional<T> = T | undefined | null/**
* Just like .filter(Boolean), but allows TypeScript to exclude falsy values in return type
* @example
* const x = [1, 2, 3, "", null, undefined, 0, false]
* const y = x.filter(BooleanFilter)
* ^? [1, 2, 3]
*/
export const BooleanFilter = <T,>(x: T): x is ValidValue<T> => Boolean(x)Unwrap Promise Type
/**
* Extracts the type from a Promise
* @example
* type PromiseNumber = Promise<number>
* type Unwrapped = UnwrapPromise<PromiseNumber>
* ^? number
*/
export type UnwrapPromise<T> = T extends Promise<infer U> ? U : TString Utilities
Capitalize First Character
/**
* Capitalizes the first character of a string
* @example
* capitaliseFirstChar('hello world') -> 'Hello world'
*/
export function capitaliseFirstChar(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}Enum to Nice String
/**
* Converts an enum to a nice string
* @example
* enumToNiceString('USER_PROFILE_DATA') -> 'User profile data'
*/
export function enumToNiceString(str: string) {
const newStr = str
.replace(/_/g, ' ')
.replace(/([A-Z])/g, '$1')
.trim()
.toLowerCase()
return newStr.charAt(0).toUpperCase() + newStr.slice(1)
}Money Formatting
Compact Money Format
/**
* Formats a number to a compact money format
* @example
* formatMoneyCompact('GBP', 123456.78) -> '£123.457K'
*/
export const formatMoneyCompact = (
currency: string | undefined,
amount: number,
) =>
new Intl.NumberFormat('en-GB', {
...(currency && {
style: 'currency',
currency: currency,
}),
notation: 'compact',
minimumFractionDigits: 0,
}).format(amount)Standard Money Format
/**
* Formats a number to a standard money format
* @example
* formatMoney('GBP', 123456.78) -> '£123,456.78'
*/
export const formatMoney = (currency: string | undefined, amount: number) => {
return new Intl.NumberFormat('en-GB', {
...(currency && {
style: 'currency',
currency: currency,
}),
minimumFractionDigits: 2,
currencyDisplay: 'narrowSymbol',
}).format(amount)
}Cookie Management
Save Cookie
/**
* Saves a cookie
* @example
* saveCookie({ key: 'myCookie', value: 'myValue' })
*/
export const saveCookie = ({
key,
value,
maxAgeInSeconds = 30 * 24 * 60 * 60, // 30 days,
}: {
key: string
value: string | null
maxAgeInSeconds?: number
}): void => {
if (value === null) {
return
}
const domain = window?.location?.hostname?.split('.').slice(-2).join('.') // e.g. "blinkist.com"
document.cookie = `${key}=${value}; path=/; domain=.${domain}; max-age=${maxAgeInSeconds};`
}Get All Cookies
/**
* Gets all cookies
* @example
* getAllCookies() -> { myCookie: 'myValue' }
*/
export const getAllCookies = (): Record<string, string> | null => {
if (typeof window === 'undefined') {
return null
}
return document.cookie.split(';').reduce(
(acc, curr) =>
Object.assign(acc, {
[curr?.split('=')?.[0]?.trim() ?? '']: curr?.split('=')?.[1] ?? '',
}),
{},
)
}Get Single Cookie
/**
* Gets a single cookie
* @example
* getCookie('myCookie') -> 'myValue'
*/
export const getCookie = (key: string): string | null => {
return getAllCookies()?.[key] ?? null
}Object Utilities
Type-Safe Object Entries
type Entries<T> = {
[K in keyof T]: [K, T[K]]
}[keyof T][]
/**
* Type-safe version of Object.entries()
* @example
* getEntries({ a: 1, b: 2 }) -> [['a', 1], ['b', 2]]
*/
export const getEntries = <T extends object>(obj: T) =>
Object.entries(obj) as Entries<T>Array Utilities
const t = Array(5)
// [ <5 empty items> ]
t.forEach((\_,i) => console.log(i))
// ❌ nothing is printed
const t = Array.from({length: 5})
// [ undefined, undefined, undefined, undefined, undefined ]
t.forEach((\_,i) => console.log(i))
// ✅ 0...4 is printed
// ✅ With initialization
const t = Array.from({length: 5}, (\_,i). => i+1)
// [1,2,3,4,5]VSCode Keybindings
Navigate to Next Blank Line using Cmd+Down or Cmd+Up
{
"key": "cmd+down",
"command": "cursorMove",
"when": "editorTextFocus",
"args": {
"to": "nextBlankLine",
"by": "wrappedLine"
}
},
{
"key": "cmd+up",
"command": "cursorMove",
"when": "editorTextFocus",
"args": {
"to": "prevBlankLine",
"by": "wrappedLine"
}
}Git Config
Make sure amends use cursor
git config --global core.editor "cursor --wait"Sort branches by committer date
git config --global branch.sort -committerdateAutomatically add fixup commits to the current branch
git config --global alias.gpfixup '!sh -c '\''git commit --fixup="$(git log --oneline | grep -v "fixup!" | head -n 1 | awk "{print \$1}")" --no-verify'\'Undo Last Commit
git config --global alias.undo 'reset --soft HEAD~1'Cursor / Claude Code Rules
# General Guidelines
## Comments
- Make heavy use of comments and JSDoc to provide product context (`why we're implementing this feature?`) and explain unusual technical solutions (`why we're using this specific approach?`)
## File Names
- Default: `$domain.$type.$extension` (example: `mocks/user.mocks.ts`, `utils/money.utils.ts, user.utils.ts`)
- React components: Use `PascalCase.tsx`
## Code Style
- Keep it short
- Keep comments, strings, and variable names short but descriptive
- Keep code flat. Minimize unnecessary `describe` blocks in tests. Example: `it('adds 1', () => {})` instead of `it('should add 1', () => {})`
- Default to use the early return pattern. Avoid nesting. If nesting becomes necessary, create a small function instead.
- Default to immutable values to avoid side effects. Create new objects instead of mutating existing ones.
- No barrel exports. Never create `index.ts` barrels—import directly from the source file. Exception: library-designed public entrypoints such as `@/components/ui/button`. https://dev.to/thepassle/a-practical-guide-against-barrel-files-for-library-authors-118c
## Tech Debt
- Follow the boy scout rule: refactor as you go when you see improvements. At least add a TODO if you don't.
- Challenge existing code: just because something was built in the past doesn't mean it deserves to be here today
- Be aggressive towards debt but pragmatic about timing
- Use JSDoc when deprecating: `@deprecated - Give a reason and an alternative`
## Naming Conventions
- **Booleans**: Prefix with `has` or `is` (`hasPermission`, `isActive`)
- **Functions**: Prefix with verbs (`get`, `extract`, `filter`)
- **Constants**: Use `SCREAMING_SNAKE_CASE`
- **Prefer positives**: `newPushNotificationsEnabled` vs `newPushNotificationsDisabled`
# JavaScript
- Use `const` over `let` to make code easier to track
- Exception: try/catch blocks where you need to set fallback values
- Prefer named exports over default exports.
- Exception: Next.js page components.
- Use `===` instead of `==` to avoid type coercion
- Exception: Use `myParam != null` (or `==`) for non-nullish comparison
# TypeScript
- Default to `type` over `interface`. Interfaces can be accidentally merged (declaration merging).
- Exception: When extending large types, use `interface` for performance
- Default to implicit types. Only use explicit types when you need to. Let TypeScript infer types from function returns.
## STRICT RULES ABOUT TYPES
- NEVER use the `as` keyword for type assertions
- NEVER use the `any` keyword, not as a type, not as a type parameter.
- Type variables correctly.
- If you can't ensure the type, use a zod schema.
- If it gets too complex use `unknown` type and flag to fix it to me with a FIXME comment.
- Build helper functions for common type operations
- If you encounter type errors, fix them properly with type guards, validation, or proper type definitions - never cast
```ts
// ❌ Bad
function process(input: any) {
return input.id;
}
// ✅ Good
const InputSchema = z.object({ id: z.string() });
function process(input: unknown) {
const data = InputSchema.parse(input);
return data.id;
}
```
### Enhancing String Types
Use Template Literals for specific formats. Example not to be used in API routes this is for typescript demonstration purposes.
```ts
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = `/api/${string}`;
function request(method: HttpMethod, route: ApiRoute) { ... }
```
# React
- Avoid massive JSX blocks and compose smaller components
- Colocate code that changes together
- Avoid 'useEffect' unless absolutely needed
- Use `shouldRender ? <div>Content</div> : null`. No `&&` in render.
# Tailwind
- Mostly use built-in values, occasionally allow dynamic values, rarely globals
- Always use v4 + global CSS file format + shaden/ui
# Next
- Prefer fetching data in RSC (page can still be static)
- Use next/font + next/script when applicable
- next/image above the fold should have 'sync' / 'eager' / use 'priority' sparingly
- Be mindful of serialized prop size for RSC → child components
# For every prompt
In all interactions be extremely concise and sacrifice grammar for the
sake of concision.
At the end of which plan, give me a list of unresolved questions to
answer, if any. Make the questions extremely concise. grammar forthe
sake of concision.