| name | typescript-conventions |
| description | TypeScript strict mode patterns, branded types, discriminated unions, and code style conventions used in Hybrid. Use when writing or reviewing TypeScript code, defining types, or enforcing code quality. |
TypeScript Conventions
Hybrid uses TypeScript 5.9+ with strict mode enabled. This skill covers patterns and conventions for writing type-safe code.
Strict Mode Configuration
All packages use strict TypeScript configuration:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true
}
}
Key Implications
- No
any — Use unknown for external/untrusted data
- Array access may be undefined —
arr[0] returns T | undefined
- Optional properties are exact —
{ foo?: string } doesn't accept { foo: undefined }
- Index signatures require bracket access —
obj.key invalid if key is from index signature
NEVER Use any
function parse(data: any) { return data.value }
const x: any = someValue
function parse(data: unknown) {
if (typeof data !== "object" || data === null) {
throw new Error("Invalid data")
}
if (!("value" in data)) {
throw new Error("Missing value")
}
return data.value
}
function isUser(data: unknown): data is User {
return typeof data === "object"
&& data !== null
&& "id" in data
&& "name" in data
}
const UserSchema = z.object({
id: z.string(),
name: z.string()
})
type User = z.infer<typeof UserSchema>
Branded Types
Use branded types for domain primitives that shouldn't be interchangeable:
declare const brand: unique symbol
type Brand<T, B> = T & { [brand]: B }
type UserId = Brand<string, "UserId">
type ConversationId = Brand<string, "ConversationId">
type InboxId = Brand<string, "InboxId">
type MessageId = Brand<string, "MessageId">
const userId = "alice" as UserId
const convId = "conv-123" as ConversationId
function getUser(id: UserId): User { ... }
function getConversation(id: ConversationId): Conversation { ... }
getUser(convId)
When to Brand
- IDs —
UserId, ConversationId, MessageId, InboxId
- Hex strings —
Address, Hex, Signature
- Timestamps —
UnixTimestamp, ISODateString
- Hashes —
Hash, CID
When NOT to Brand
- Regular strings that don't represent domain concepts
- Numbers used as counters or indices
- Generic configuration values
Discriminated Unions
Use discriminated unions for variant types:
type CronSchedule =
| { kind: "at"; at: string }
| { kind: "every"; everyMs: number; anchorMs?: number }
| { kind: "cron"; expr: string; tz?: string; staggerMs?: number }
function getNextRun(schedule: CronSchedule): Date {
switch (schedule.kind) {
case "at":
return new Date(schedule.at)
case "every":
return new Date(Date.now() + schedule.everyMs)
case "cron":
return parseCron(schedule.expr)
}
}
type Schedule =
| { type: "once"; time: string }
| { type: "recurring"; interval: number }
function getNextRun(schedule: Schedule): Date {
if (schedule.type === "once") {
return new Date(schedule.time)
}
return new Date(Date.now() + schedule.interval)
}
Exhaustive Checking
function getStatusText(status: ScheduleStatus): string {
switch (status) {
case "pending": return "Pending"
case "running": return "Running"
case "completed": return "Completed"
case "failed": return "Failed"
default:
const _exhaustive: never = status
return _exhaustive
}
}
Type Guards & Assertions
function isCronSchedule(value: unknown): value is CronSchedule {
if (typeof value !== "object" || value === null) return false
if (!("kind" in value)) return false
const kind = (value as { kind: unknown }).kind
return kind === "at" || kind === "every" || kind === "cron"
}
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new TypeError("Expected User")
}
}
const data: unknown = JSON.parse(input)
if (isCronSchedule(data)) {
}
assertIsUser(data)
Generic Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
function createMap<T extends PropertyKey, U>(entries: [T, U][]): Record<T, U> {
return Object.fromEntries(entries) as Record<T, U>
}
type ApiResponse<T> = T extends { error: string }
? { success: false; error: string }
: { success: true; data: T }
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
Error Handling
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
async function tryAsync<T>(
fn: () => Promise<T>
): Promise<Result<T>> {
try {
return { ok: true, value: await fn() }
} catch (e) {
return { ok: false, error: e instanceof Error ? e : new Error(String(e)) }
}
}
const result = await tryAsync(() => fetchUser(id))
if (result.ok) {
console.log(result.value)
} else {
console.error(result.error)
}
class ValidationError extends Error {
constructor(message: string, public field: string) {
super(message)
this.name = "ValidationError"
}
}
try {
await processJob(job)
} catch (e) {
if (e instanceof ValidationError) {
} else if (e instanceof NetworkError) {
} else {
throw e
}
}
Interface vs Type
interface User {
id: string
name: string
email: string
}
interface UserWithTimestamps extends User {
createdAt: Date
updatedAt: Date
}
type Status = "pending" | "running" | "completed"
type UserWithRole = User & { role: Role }
type PartialUser = Partial<User>
type UserKeys = keyof User
Async Patterns
async function fetchUser(id: string): Promise<User | null> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) return null
return response.json()
} catch (e) {
console.error("Failed to fetch user:", e)
return null
}
}
const [users, conversations] = await Promise.all([
fetchUsers(),
fetchConversations()
])
const results = await Promise.allSettled([
fetchUser(id1),
fetchUser(id2),
fetchUser(id3)
])
const succeeded = results
.filter((r): r is PromiseFulfilledResult<User> => r.status === "fulfilled")
.map(r => r.value)
type AsyncFunction<T, Args extends unknown[]> = (...args: Args) => Promise<T>
Null/Undefined Handling
const name = user?.profile?.name
const timeout = options.timeout ?? 5000
function init(config?: Config) {
config ??= defaultConfig
return config
}
function process(value: string | null | undefined): string {
if (value == null) return "default"
return value.toUpperCase()
}
function findUser(id: string): User | undefined {
return users.get(id)
}
Array Safety
const first = arr[0]
const first = arr[0]
if (first !== undefined) {
}
const first = arr.at(0)
return first ?? defaultValue
const defined = arr.filter((x): x is T => x !== undefined)
const ids = users.map(u => u.id)
const first = ids[0]
const name = users.find(u => u.id === id)?.name
Object Types
const config = {
port: 8454,
host: "localhost"
} satisfies Record<string, string | number>
const STATUS = {
PENDING: "pending",
RUNNING: "running",
COMPLETED: "completed"
} as const
type Status = typeof STATUS[keyof typeof STATUS]
type UserMap = Record<UserId, User>
type StringMap = Record<string, string>
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates }
}
Function Types
function parseConfig(input: string): Config {
}
const double = (x: number) => x * 2
function parse(input: string): Config
function parse(input: string, strict: true): Config
function parse(input: string, strict: false): Partial<Config>
function parse(input: string, strict = true) {
}
function identity<T>(value: T): T {
return value
}
Import Patterns
import { User, Conversation } from "@hybrd/types"
import type { CronSchedule, CronJob } from "@hybrd/types"
import * as fs from "node:fs/promises"
import { z } from "zod"
import "./setup"
Module Patterns
export { User, Conversation } from "./user"
export type { UserId, ConversationId } from "./branded"
export { isUser } from "./guards"
export * as API from "./api"
export { User } from "./user"
export { Conversation } from "./conversation"
export type { UserId } from "./branded"
Zod Integration
import { z } from "zod"
const UserSchema = z.object({
id: z.string(),
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(["owner", "guest"]).default("guest"),
metadata: z.record(z.unknown()).optional()
})
type User = z.infer<typeof UserSchema>
type UserInput = z.input<typeof UserSchema>
type UserOutput = z.output<typeof UserSchema>
const user = UserSchema.parse(data)
const result = UserSchema.safeParse(data)
const TrimmedUserSchema = UserSchema.transform(user => ({
...user,
name: user.name.trim()
}))
const ScheduleSchema = z.object({
kind: z.enum(["at", "every", "cron"]),
at: z.string().datetime().optional(),
everyMs: z.number().positive().optional(),
expr: z.string().optional()
}).refine(
(data) => {
if (data.kind === "at") return !!data.at
if (data.kind === "every") return !!data.everyMs
if (data.kind === "cron") return !!data.expr
return false
},
{ message: "Invalid schedule configuration" }
)
Testing Patterns
import { describe, it, expectType } from "vitest"
describe("types", () => {
it("UserId is branded string", () => {
type Test = UserId extends string ? true : false
expectType<Test>(true)
})
})
type MockConversation = {
id: ConversationId
messages: MockMessage[]
send: (content: string) => Promise<void>
}
function createMockConversation(overrides?: Partial<MockConversation>): MockConversation {
return {
id: "conv-123" as ConversationId,
messages: [],
send: async () => {},
...overrides
}
}
Common Pitfalls
1. Forgetting undefined in Array Access
const first = arr[0].toUpperCase()
const first = arr[0]
if (first) {
first.toUpperCase()
}
2. Object Property Access
const value = obj[key]
const value = obj[key] ?? defaultValue
3. JSON.parse Returns unknown
const data = JSON.parse(input) as User[]
const data = UserSchema.array().parse(JSON.parse(input))
4. Optional Property vs Undefined Value
interface Config {
timeout?: number
retries?: number | undefined
}
const config1: Config = {}
const config2: Config = { timeout: undefined }
const config3: Config = { timeout: 5000 }
Biome Lint Rules
Run pnpm lint to check. Key rules:
noUnusedVariables
noImplicitAny
useConst
noNonNullAssertion
noUnsafeDeclarationMerging
useExplicitType
Fix with pnpm lint:fix for auto-fixable issues.