Writing TypeScript
Language-level TypeScript for this project: strict typing, parse-don't-validate at boundaries, Result types, Bun + Vite + Vitest. Frontend is Svelte 5 + SvelteKit 2, not React — defer to svelte-skills:* for everything Svelte-specific.
Scope routing
| If you need to… |
Read |
Configure tsconfig.json, set up Bun/Vite, organize imports, file naming |
references/code-style.md |
Use unknown/generics/discriminated unions/type guards, narrow types, satisfies vs as |
references/type-safety.md |
| Look up everyday patterns — Result types, Zod validation, async, custom errors, module organization |
references/patterns.md |
| Audit code against Clean Code rules (functions, names, comments, general, tests) |
references/clean-code.md |
| Write tests with Vitest (unit, async, mocking, coverage, boundary cases) |
references/testing.md |
House style — non-negotiable
- Strict mode everywhere.
strict: true, noUncheckedIndexedAccess: true, exactOptionalPropertyTypes: true. No exceptions.
unknown, never any. any opts out of the type system silently. unknown forces narrowing before use.
- Parse, don't validate, at boundaries. Untrusted data (HTTP response, env, message payload) gets a Zod parse — afterwards it's a typed value.
- Composition over inheritance. Small focused functions, types/interfaces for shape, no class hierarchies for business logic.
satisfies for validation, as only as a last resort. as widens silently; satisfies preserves literal types AND verifies shape.
bun for everything Node-level. Not npm, not pnpm, not yarn — bun install, bun run, bun test.
- No
enums. Use literal union types (type Role = 'admin' | 'user'). Enums emit runtime code and don't narrow as cleanly.
- Result types over throw for expected failures. Throw for bugs; return
Result<T, E> for expected failures (validation, not found, conflict).
Quick patterns
// Types — modern syntax, no `any`, prefer literal unions
type Role = 'admin' | 'user' | 'guest';
type User = { id: string; email: string; role: Role };
// Type guard — narrows unknown to a typed shape
function isUser(value: unknown): value is User {
return typeof value === 'object' && value !== null && 'id' in value;
}
// Discriminated union — exhaustive switch with `never` check
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function unwrap<T>(r: Result<T>): T {
if (r.ok) return r.value;
throw r.error;
}
// satisfies — verifies shape without widening
const ROLES = {
admin: { canEdit: true, canDelete: true },
user: { canEdit: true, canDelete: false },
guest: { canEdit: false, canDelete: false },
} satisfies Record<Role, { canEdit: boolean; canDelete: boolean }>;
// ROLES.admin.canEdit is still typed as `true`, not `boolean`
Toolchain
bun install # install deps from bun.lock
bun add <pkg> # add a runtime dep
bun add -d <pkg> # add a dev dep
bun run build # build (vite)
bun run dev # dev server (vite)
bun test # vitest
bun run lint # eslint
bun run format # prettier
Project layout
| Location |
Purpose |
Notes |
components/apps/frontend/ |
SvelteKit 2 application |
+page.svelte routes, +page.ts loaders. See svelte-skills:sveltekit-structure. |
packages/oxen_componets/ |
Shared Svelte 5 component library |
Published via Bun workspaces. See svelte-skills:svelte-components. |
packages/oxen_componets/vite.config.ts |
Library build config |
Bun + Vite. |
Cross-skill boundaries
svelte-skills:svelte-runes — $state, $derived, $effect, $props. Anything reactivity-related goes there.
svelte-skills:sveltekit-structure — routes, layouts, error boundaries, SSR.
svelte-skills:sveltekit-data-flow — load functions, form actions, +page.server.ts vs +page.ts.
svelte-skills:sveltekit-remote-functions — .remote.ts files with command() / query() / form().
svelte-skills:svelte-components — component composition, Bits UI / Ark UI / Melt UI patterns.
svelte-skills:svelte-styling — scoped styles, :global, CSS custom properties.
svelte-skills:svelte-template-directives — {@attach}, {@render}, {@const}.
svelte-skills:layerchart-svelte5 — LayerChart snippets and tooltips.
- This skill is for TypeScript itself — types, language idioms, project tooling. If your question is about Svelte syntax, runes, or SvelteKit, read the sibling skill first.
Boy Scout rule
Every TypeScript change leaves the touched code a little cleaner. Not perfect — better.
- Rename a cryptic variable →
clean-code.md § Names
- Delete commented-out code →
clean-code.md § Comments (C5)
- Replace a magic number with a named constant →
clean-code.md § General (G25)
- Extract a function that's doing two things →
clean-code.md § Functions (F3, G30)
Keep changes proportional to the task. Don't refactor unrelated modules; do clean up what you're already editing.
Top gotchas
as silently widens; satisfies preserves literals. const x = { foo: 1 } as { foo: number } loses 1 (becomes number). Use satisfies when you want both validation AND narrow types.
Array<T>.includes(x) requires x: T. Narrowing-from-union doesn't work. Wrap in a type-predicate helper: function isRole(v: string): v is Role { return ROLES.includes(v as Role); }.
tsconfig.json extends doesn't merge compilerOptions.paths. Child paths replace parent paths entirely. Re-declare everything in the child.
import type with verbatimModuleSyntax + named exports breaks if you mix value and type imports. Use inline: import { type Foo, bar } from "mod".
strictNullChecks: false is contagious. Without it, T silently means T | null | undefined for every type. Partial migrations leave types that lie about nullability — keep it true always.
as const ≠ as Foo ≠ satisfies Foo. as const freezes + narrows to literals. as Foo asserts (lying allowed). satisfies Foo validates without widening. Use as const for lookup tables, satisfies for typed configs, never as without a comment explaining why.
- Mutable default parameters share state in arrow functions too.
const f = (xs: number[] = []) => xs.push(1) reuses the same array. Use xs?: number[] and create inside.
- Bun-only APIs leak into shared code.
Bun.file(), Bun.serve() aren't available in browser bundles. Frontend code must stay Bun-API-free.
1---2name: writing-typescript3description: Idiomatic TypeScript for this project — strict typing, `unknown` over `any`, Result types over throwing, composition, Bun/Vite/Vitest toolchain. Use when writing or reviewing TypeScript code in the SvelteKit frontend (`components/apps/frontend/`) or the component library (`packages/oxen_componets/`). Defers Svelte-specific concerns (runes, routing, components) to the `svelte-skills:*` plugin.4---56# Writing TypeScript78Language-level TypeScript for this project: strict typing, parse-don't-validate at boundaries, Result types, Bun + Vite + Vitest. Frontend is **Svelte 5 + SvelteKit 2**, not React — defer to `svelte-skills:*` for everything Svelte-specific.910## Scope routing1112| If you need to… | Read |13| --------------------------------------------------------------------------------------------------- | --------------------------- |14| Configure `tsconfig.json`, set up Bun/Vite, organize imports, file naming | `references/code-style.md` |15| Use `unknown`/generics/discriminated unions/type guards, narrow types, `satisfies` vs `as` | `references/type-safety.md` |16| Look up everyday patterns — Result types, Zod validation, async, custom errors, module organization | `references/patterns.md` |17| Audit code against Clean Code rules (functions, names, comments, general, tests) | `references/clean-code.md` |18| Write tests with Vitest (unit, async, mocking, coverage, boundary cases) | `references/testing.md` |1920## House style — non-negotiable2122- **Strict mode everywhere.** `strict: true`, `noUncheckedIndexedAccess: true`, `exactOptionalPropertyTypes: true`. No exceptions.23- **`unknown`, never `any`.** `any` opts out of the type system silently. `unknown` forces narrowing before use.24- **Parse, don't validate, at boundaries.** Untrusted data (HTTP response, env, message payload) gets a Zod parse — afterwards it's a typed value.25- **Composition over inheritance.** Small focused functions, types/interfaces for shape, no class hierarchies for business logic.26- **`satisfies` for validation, `as` only as a last resort.** `as` widens silently; `satisfies` preserves literal types AND verifies shape.27- **`bun` for everything Node-level.** Not `npm`, not `pnpm`, not `yarn` — `bun install`, `bun run`, `bun test`.28- **No `enum`s.** Use literal union types (`type Role = 'admin' | 'user'`). Enums emit runtime code and don't narrow as cleanly.29- **Result types over throw for expected failures.** Throw for _bugs_; return `Result<T, E>` for _expected_ failures (validation, not found, conflict).3031## Quick patterns3233```typescript34// Types — modern syntax, no `any`, prefer literal unions35type Role = 'admin' | 'user' | 'guest';36type User = { id: string; email: string; role: Role };3738// Type guard — narrows unknown to a typed shape39function isUser(value: unknown): value is User {40 return typeof value === 'object' && value !== null && 'id' in value;41}4243// Discriminated union — exhaustive switch with `never` check44type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };4546function unwrap<T>(r: Result<T>): T {47 if (r.ok) return r.value;48 throw r.error;49}5051// satisfies — verifies shape without widening52const ROLES = {53 admin: { canEdit: true, canDelete: true },54 user: { canEdit: true, canDelete: false },55 guest: { canEdit: false, canDelete: false },56} satisfies Record<Role, { canEdit: boolean; canDelete: boolean }>;57// ROLES.admin.canEdit is still typed as `true`, not `boolean`58```5960## Toolchain6162```bash63bun install # install deps from bun.lock64bun add <pkg> # add a runtime dep65bun add -d <pkg> # add a dev dep66bun run build # build (vite)67bun run dev # dev server (vite)68bun test # vitest69bun run lint # eslint70bun run format # prettier71```7273## Project layout7475| Location | Purpose | Notes |76| ---------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------- |77| `components/apps/frontend/` | SvelteKit 2 application | `+page.svelte` routes, `+page.ts` loaders. See `svelte-skills:sveltekit-structure`. |78| `packages/oxen_componets/` | Shared Svelte 5 component library | Published via Bun workspaces. See `svelte-skills:svelte-components`. |79| `packages/oxen_componets/vite.config.ts` | Library build config | Bun + Vite. |8081## Cross-skill boundaries8283- **`svelte-skills:svelte-runes`** — `$state`, `$derived`, `$effect`, `$props`. Anything reactivity-related goes there.84- **`svelte-skills:sveltekit-structure`** — routes, layouts, error boundaries, SSR.85- **`svelte-skills:sveltekit-data-flow`** — load functions, form actions, `+page.server.ts` vs `+page.ts`.86- **`svelte-skills:sveltekit-remote-functions`** — `.remote.ts` files with `command()` / `query()` / `form()`.87- **`svelte-skills:svelte-components`** — component composition, Bits UI / Ark UI / Melt UI patterns.88- **`svelte-skills:svelte-styling`** — scoped styles, `:global`, CSS custom properties.89- **`svelte-skills:svelte-template-directives`** — `{@attach}`, `{@render}`, `{@const}`.90- **`svelte-skills:layerchart-svelte5`** — LayerChart snippets and tooltips.91- This skill is for **TypeScript itself** — types, language idioms, project tooling. If your question is about Svelte syntax, runes, or SvelteKit, read the sibling skill first.9293## Boy Scout rule9495Every TypeScript change leaves the touched code a little cleaner. Not perfect — better.9697- Rename a cryptic variable → `clean-code.md` § Names98- Delete commented-out code → `clean-code.md` § Comments (C5)99- Replace a magic number with a named constant → `clean-code.md` § General (G25)100- Extract a function that's doing two things → `clean-code.md` § Functions (F3, G30)101102Keep changes proportional to the task. Don't refactor unrelated modules; do clean up what you're already editing.103104## Top gotchas105106- **`as` silently widens; `satisfies` preserves literals.** `const x = { foo: 1 } as { foo: number }` loses `1` (becomes `number`). Use `satisfies` when you want both validation AND narrow types.107- **`Array<T>.includes(x)` requires `x: T`.** Narrowing-from-union doesn't work. Wrap in a type-predicate helper: `function isRole(v: string): v is Role { return ROLES.includes(v as Role); }`.108- **`tsconfig.json` `extends` doesn't merge `compilerOptions.paths`.** Child paths _replace_ parent paths entirely. Re-declare everything in the child.109- **`import type` with `verbatimModuleSyntax` + named exports** breaks if you mix value and type imports. Use inline: `import { type Foo, bar } from "mod"`.110- **`strictNullChecks: false` is contagious.** Without it, `T` silently means `T | null | undefined` for _every_ type. Partial migrations leave types that lie about nullability — keep it `true` always.111- **`as const` ≠ `as Foo` ≠ `satisfies Foo`.** `as const` freezes + narrows to literals. `as Foo` asserts (lying allowed). `satisfies Foo` validates without widening. Use `as const` for lookup tables, `satisfies` for typed configs, never `as` without a comment explaining why.112- **Mutable default parameters share state in arrow functions too.** `const f = (xs: number[] = []) => xs.push(1)` reuses the same array. Use `xs?: number[]` and create inside.113- **Bun-only APIs leak into shared code.** `Bun.file()`, `Bun.serve()` aren't available in browser bundles. Frontend code must stay Bun-API-free.