| name | pinpoint-typescript |
| description | TypeScript strictest patterns, type guards, optional properties (exactOptionalPropertyTypes), Drizzle query safety, null checks. Use when fixing type errors, implementing complex types, or when user mentions TypeScript/types/generics. |
PinPoint TypeScript Guide
When to Use This Skill
Use this skill when:
- Fixing TypeScript compilation errors
- Implementing complex types or generics
- Dealing with optional properties and
exactOptionalPropertyTypes
- Working with Drizzle ORM types
- Handling null/undefined safety
- User mentions: "TypeScript", "type error", "type guard", "optional", "nullable"
Quick Reference
Critical TypeScript Rules
- Strictest config: No
any, no !, no unsafe as (NON_NEGOTIABLE #7)
- Explicit return types: Required for public functions
- Path aliases: Always use
~/ (e.g., ~/lib/utils)
- Optional property assignment: Use conditional spread for
exactOptionalPropertyTypes safety
- Type guards: Use predicates for narrowing (e.g.,
profile is UserProfile)
Common Fixes
Optional Properties (exactOptionalPropertyTypes):
const data = {
id: uuid(),
...(name && { name }),
...(description && { description }),
};
const data = { name: value };
Detailed Documentation
Read these files for comprehensive TypeScript patterns and rules:
docs/TYPESCRIPT_STRICTEST_PATTERNS.md — Detailed generic and union patterns
docs/NON_NEGOTIABLES.md — Core project constraints (CORE-TS-* rules)
src/lib/types/database.ts — Canonical database entity types
Core Safety Patterns
Null Safety & Optional Chaining
const firstItem = items[0]?.name ?? "No items";
const lastItem = items.at(-1)?.name ?? "No items";
const machineName = issue.machine?.name ?? "Unknown";
Date Formatting (Narrowing Required)
Helpers in ~/lib/dates.ts (formatDate, formatDateTime, formatRelative) currently throw TypeError on null or undefined. You MUST narrow before calling.
<span>{machine.fixedAt ? formatDate(machine.fixedAt) : "—"}</span>
if (!issue.createdAt) return null;
return <div>{formatDate(issue.createdAt)}</div>
<div>{formatDate(issue.closedAt)}</div>
Type Guards
function hasItems<T>(arr: T[] | null | undefined): arr is T[] {
return !!arr && arr.length > 0;
}
import { type UserProfile } from "~/lib/types/database";
function isUserProfile(profile: unknown): profile is UserProfile {
return (
typeof profile === "object" &&
profile !== null &&
"id" in profile &&
"email" in profile &&
"role" in profile
);
}
type Result =
| { type: "success"; data: string }
| { type: "error"; message: string };
function processResult(result: Result) {
if (result.type === "success") {
console.log(result.data);
} else {
console.log(result.message);
}
}
Drizzle Query Safety
Safe Query Patterns
import { eq, desc } from "drizzle-orm";
import { db } from "~/server/db";
import { issues, userProfiles } from "~/server/db/schema";
import { type Issue } from "~/lib/types/database";
export async function getIssuesByMachine(machineId: string): Promise<Issue[]> {
return await db.query.issues.findMany({
where: eq(issues.machineId, machineId),
orderBy: desc(issues.createdAt),
});
}
Database Type Inference
import { userProfiles } from "~/server/db/schema";
import { type InferSelectModel } from "drizzle-orm";
type DbUser = InferSelectModel<typeof userProfiles>;
import { type UserRole } from "~/lib/types/user";
export type UserProfileSummary = {
id: string;
email: string;
fullName: string;
role: UserRole;
};
export function toProfileSummary(dbUser: DbUser): UserProfileSummary {
return {
id: dbUser.id,
email: dbUser.email,
fullName: `${dbUser.firstName} ${dbUser.lastName}`,
role: dbUser.role,
};
}
Supabase SSR Safety
Safe Auth Context
import { createClient } from "~/lib/supabase/server";
import { redirect } from "next/navigation";
export default async function ProtectedPage() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect("/login");
}
return <DashboardContent user={user} />;
}
Server Action Safety
"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { eq } from "drizzle-orm";
import { db } from "~/server/db";
import { userProfiles } from "~/server/db/schema";
import { createClient } from "~/lib/supabase/server";
export async function updateProfile(formData: FormData): Promise<void> {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
redirect("/login");
}
const firstName = formData.get("firstName");
if (typeof firstName !== "string") {
throw new Error("First name must be a string");
}
await db
.update(userProfiles)
.set({ firstName })
.where(eq(userProfiles.id, user.id));
revalidatePath("/profile");
}
Strict Function Signatures
import { type Issue } from "~/lib/types/database";
import { db } from "~/server/db";
import { issues } from "~/server/db/schema";
import { eq } from "drizzle-orm";
export async function getIssuesByMachine(machineId: string): Promise<Issue[]> {
return await db.query.issues.findMany({
where: eq(issues.machineId, machineId),
});
}
import { publicIssueSchema } from "~/app/(app)/report/schemas";
Anti-Patterns to Avoid
const data: any = await fetchData();
const user = getUser()!.email;
const result = dangerousOperation();
const user = data as UserProfile;
if (isUserProfile(data)) {
const user = data;
}
Server Components Safety
Async Component Patterns
import { db } from "~/server/db";
import { issues } from "~/server/db/schema";
import { eq, desc } from "drizzle-orm";
export default async function MachineIssuesPage({
params
}: {
params: Promise<{ machineId: string }>
}): Promise<JSX.Element> {
const { machineId } = await params;
const issuesResult = await db.query.issues.findMany({
where: eq(issues.machineId, machineId),
orderBy: desc(issues.createdAt),
});
return (
<div>
{issuesResult.map((issue) => (
<IssueCard key={issue.id} issue={issue} />
))}
</div>
);
}
Form Data Validation (Zod)
"use server";
import { revalidatePath } from "next/cache";
import { db } from "~/server/db";
import { issues } from "~/server/db/schema";
import { publicIssueSchema } from "~/app/(app)/report/schemas";
export async function createIssueAction(formData: FormData) {
const rawData = {
machineId: String(formData.get("machineId")),
title: String(formData.get("title")),
};
const validation = publicIssueSchema.safeParse(rawData);
if (!validation.success) {
return { error: validation.error.flatten() };
}
const [issue] = await db.insert(issues).values(validation.data).returning();
revalidatePath("/issues");
return issue;
}
TypeScript Checklist
Before committing TypeScript code:
Additional Resources
- TypeScript patterns:
docs/TYPESCRIPT_STRICTEST_PATTERNS.md
- Non-negotiables:
docs/NON_NEGOTIABLES.md (CORE-TS-* rules)
- Drizzle types: Use
src/lib/types/database.ts as canonical source