Claude Code Plugins

Community-maintained marketplace

Feedback

typescript-strict-checks

@wendleyw/brianna-dawes-studio-app
0
0

Use when fixing TypeScript errors, implementing new features, or reviewing code. Ensures adherence to strict mode with noUncheckedIndexedAccess and exactOptionalPropertyTypes. Automatically applied to all TypeScript work.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name typescript-strict-checks
description Use when fixing TypeScript errors, implementing new features, or reviewing code. Ensures adherence to strict mode with noUncheckedIndexedAccess and exactOptionalPropertyTypes. Automatically applied to all TypeScript work.
allowed-tools Read, Edit, Grep, Glob, Bash

TypeScript Strict Checks Skill

Overview

This skill enforces TypeScript strict mode compliance for the project, with special focus on noUncheckedIndexedAccess and exactOptionalPropertyTypes which cause the most common errors.


Project TypeScript Configuration

File: tsconfig.json

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "alwaysStrict": true
  }
}

Key Settings That Cause Errors:

  • noUncheckedIndexedAccess: Array/object access returns T | undefined
  • exactOptionalPropertyTypes: Optional properties must explicitly include undefined when set

Common Error 1: noUncheckedIndexedAccess

The Problem

Array and object index access returns T | undefined instead of T:

const users = ['Alice', 'Bob', 'Charlie'];
const first = users[0]; // Type: string | undefined (not string!)

const obj = { name: 'Alice' };
const value = obj['name']; // Type: string | undefined

Solutions

Solution 1: Check for undefined

const first = users[0];
if (first === undefined) {
  logger.error('No users found');
  return;
}
// Now TypeScript knows first is string
console.log(first.toUpperCase()); // ✅ Safe

Solution 2: Optional Chaining

// Access property safely
const name = users[0]?.toUpperCase();
// Type: string | undefined

// With nullish coalescing
const name = users[0]?.toUpperCase() ?? 'Unknown';
// Type: string

Solution 3: Non-null Assertion (Use Sparingly!)

// Only when you're 100% certain it exists
const first = users[0]!;
// Type: string (assertion removes undefined)

// ⚠️ WARNING: Use only when you control the array and know it's non-empty

Solution 4: Array Methods (Preferred)

// ✅ Use array methods that handle undefined
users.forEach(user => {
  // user is string, not string | undefined
  console.log(user.toUpperCase());
});

users.map(user => user.toUpperCase());

const first = users.find(u => u === 'Alice');
// Type: string | undefined (explicitly)

Real Example from Codebase

// ❌ WRONG
function getProjectStatus(projects: Project[]) {
  return projects[0].status; // Error: Object is possibly undefined
}

// ✅ CORRECT - Option 1: Guard
function getProjectStatus(projects: Project[]) {
  const first = projects[0];
  if (!first) return null;
  return first.status;
}

// ✅ CORRECT - Option 2: Optional chaining
function getProjectStatus(projects: Project[]) {
  return projects[0]?.status ?? null;
}

// ✅ CORRECT - Option 3: Array method
function getProjectStatus(projects: Project[]) {
  return projects.at(0)?.status ?? null;
}

Common Error 2: exactOptionalPropertyTypes

The Problem

Optional properties can't be set to undefined unless undefined is explicitly in the type:

interface User {
  name: string;
  email?: string; // Can be omitted, but can't be set to undefined!
}

// ❌ Error: Type 'undefined' is not assignable to type 'string'
const user: User = { name: 'Alice', email: undefined };

// ✅ OK: Omit the property
const user: User = { name: 'Alice' };

Solutions

Solution 1: Explicit undefined in Type

interface User {
  name: string;
  email?: string | undefined; // Now can be omitted OR undefined
}

// ✅ Both work
const user1: User = { name: 'Alice' };
const user2: User = { name: 'Bob', email: undefined };

Solution 2: Omit Property Instead of Setting to undefined

interface User {
  name: string;
  email?: string;
}

// ❌ Don't do this
const user = { name: 'Alice', email: undefined };

// ✅ Do this
const user = { name: 'Alice' };

// ✅ Or conditionally include
const user = {
  name: 'Alice',
  ...(email && { email }),
};

Solution 3: Use Required + Optional Pattern

// Instead of optional with undefined
interface User {
  name: string;
  email: string | null; // Required but nullable
}

const user: User = { name: 'Alice', email: null }; // ✅ Clear intent

Real Example from Codebase

// ❌ WRONG
interface ProjectFormData {
  title: string;
  description?: string;
  clientId?: string;
}

function createProject(data: ProjectFormData) {
  return {
    title: data.title,
    description: data.description ?? undefined, // Error!
    clientId: data.clientId ?? undefined, // Error!
  };
}

// ✅ CORRECT - Option 1: Explicit undefined
interface ProjectFormData {
  title: string;
  description?: string | undefined;
  clientId?: string | undefined;
}

function createProject(data: ProjectFormData) {
  return {
    title: data.title,
    description: data.description ?? undefined, // ✅ OK
    clientId: data.clientId ?? undefined, // ✅ OK
  };
}

// ✅ CORRECT - Option 2: Conditional spread
interface ProjectFormData {
  title: string;
  description?: string;
  clientId?: string;
}

function createProject(data: ProjectFormData) {
  return {
    title: data.title,
    ...(data.description && { description: data.description }),
    ...(data.clientId && { clientId: data.clientId }),
  };
}

// ✅ CORRECT - Option 3: Use null
interface ProjectFormData {
  title: string;
  description: string | null;
  clientId: string | null;
}

function createProject(data: ProjectFormData) {
  return {
    title: data.title,
    description: data.description,
    clientId: data.clientId,
  };
}

Common Error 3: Supabase Array/Object Returns

The Problem

Supabase returns arrays from .select(), but with noUncheckedIndexedAccess you must handle potentially empty arrays:

const { data } = await supabase
  .from('projects')
  .select('*')
  .eq('id', projectId);

// ❌ Error: data is Project[] | null, and data[0] is Project | undefined
const project = data[0];

Solution: Use .single() for Single Records

// ✅ CORRECT: Use .single() when expecting one record
const { data, error } = await supabase
  .from('projects')
  .select('*')
  .eq('id', projectId)
  .single(); // Returns Project | null, not Project[]

if (error || !data) {
  logger.error('Project not found', { projectId, error });
  return null;
}

// data is Project, not Project | undefined
return data;

Solution: Check Array Before Access

// When you need to use select without .single()
const { data } = await supabase
  .from('projects')
  .select('*')
  .eq('status', 'active');

if (!data || data.length === 0) {
  return [];
}

// Now safe to use data
return data.map(project => project.title);

Common Error 4: React Query with Strict Types

The Problem

React Query hooks return data that can be undefined during loading:

const { data } = useQuery({
  queryKey: projectKeys.detail(projectId),
  queryFn: () => projectService.getById(projectId),
});

// ❌ Error: data is Project | undefined
return <div>{data.title}</div>;

Solutions

Solution 1: Loading State Check

const { data, isLoading } = useQuery({
  queryKey: projectKeys.detail(projectId),
  queryFn: () => projectService.getById(projectId),
});

if (isLoading || !data) {
  return <Spinner />;
}

// Now data is Project
return <div>{data.title}</div>;

Solution 2: Optional Chaining in JSX

const { data } = useQuery({
  queryKey: projectKeys.detail(projectId),
  queryFn: () => projectService.getById(projectId),
});

return <div>{data?.title ?? 'Loading...'}</div>;

Solution 3: Type Guard Component

function ProjectView({ projectId }: { projectId: string }) {
  const { data, isLoading } = useQuery({
    queryKey: projectKeys.detail(projectId),
    queryFn: () => projectService.getById(projectId),
  });

  if (isLoading) return <Spinner />;
  if (!data) return <EmptyState message="Project not found" />;

  // Data is guaranteed Project here
  return <ProjectDetails project={data} />;
}

Key Patterns in Codebase

Pattern 1: Query Keys Factory (as const)

Location: src/features/*/services/*Keys.ts

// ✅ CORRECT: Use as const for strict typing
export const projectKeys = {
  all: ['projects'] as const,
  lists: () => [...projectKeys.all, 'list'] as const,
  list: (params: ProjectsQueryParams) =>
    [...projectKeys.lists(), params] as const,
  details: () => [...projectKeys.all, 'detail'] as const,
  detail: (id: string) => [...projectKeys.details(), id] as const,
} as const;

// Type is readonly, prevents accidental mutations

Pattern 2: Service Method Return Types

Location: src/features/*/services/*.ts

// ✅ CORRECT: Explicit return types
export class ProjectService {
  async getById(id: string): Promise<Project | null> {
    const { data, error } = await supabase
      .from('projects')
      .select('*')
      .eq('id', id)
      .single();

    if (error) {
      logger.error('Failed to fetch project', { id, error });
      return null;
    }

    return data; // Type: Project | null
  }

  async list(params: ProjectsQueryParams): Promise<Project[]> {
    const { data, error } = await supabase
      .from('projects')
      .select('*');

    if (error || !data) {
      logger.error('Failed to fetch projects', { error });
      return [];
    }

    return data; // Type: Project[]
  }
}

Pattern 3: Zod Schemas with Strict Types

Location: src/features/*/domain/*.ts

import { z } from 'zod';

// ✅ CORRECT: Use .nullable() or .optional() explicitly
export const projectSchema = z.object({
  id: z.string().uuid(),
  title: z.string().min(1),
  description: z.string().nullable(), // Can be null
  clientId: z.string().uuid().optional(), // Can be omitted
  status: z.enum(['draft', 'in_progress', 'review', 'done', 'archived']),
  createdAt: z.string().datetime(),
});

export type Project = z.infer<typeof projectSchema>;
// Result:
// {
//   id: string;
//   title: string;
//   description: string | null;
//   clientId?: string;
//   status: "draft" | "in_progress" | ...;
//   createdAt: string;
// }

Testing for Compliance

Commands

# Type check only (fast)
npm run typecheck

# Type check + build (slower, catches more issues)
npm run build

# Watch mode during development
npm run typecheck -- --watch

VS Code Integration

The project has strict TypeScript checks enabled in VS Code. Errors show inline:

  • Red squiggles for type errors
  • Hover for detailed error messages
  • Quick fixes available (Cmd/Ctrl + .)

Common Fixes Checklist

When fixing TypeScript errors:

  • Is it array/object access? → Add undefined check or optional chaining
  • Is it optional property? → Add | undefined to type or use conditional spread
  • Is it Supabase query? → Use .single() or check array length
  • Is it React Query? → Add loading/undefined checks
  • Is it a service method? → Explicit return type with | null
  • Is it a Zod schema? → Use .nullable() or .optional()

When to Use Non-null Assertion (!)

Use sparingly! Only when:

  1. You control the data source and know it's non-empty
  2. Immediately after checking in same scope:
if (array.length > 0) {
  const first = array[0]!; // ✅ Safe, we just checked length
}
  1. Constants that are always defined:
const ADMIN_ROLE = 'admin' as const;
const roles = [ADMIN_ROLE];
const adminRole = roles[0]!; // ✅ Safe, constant array

❌ DON'T use for:

  • User input
  • API responses
  • Database queries
  • External data sources

Legacy Code Exclusion

Location: /src/legacy/

The legacy directory is excluded from TypeScript compilation:

// tsconfig.json
{
  "exclude": ["src/legacy/**/*"]
}

Don't modify legacy code. Migrate to new feature-based structure instead.


Quick Reference

Array Access

// ❌ WRONG
const first = array[0];

// ✅ CORRECT
const first = array[0];
if (!first) return;

// OR
const first = array[0] ?? defaultValue;

// OR
const first = array.at(0);

Optional Properties

// ❌ WRONG
interface Props {
  value?: string;
}
const obj: Props = { value: undefined };

// ✅ CORRECT
interface Props {
  value?: string | undefined;
}
const obj: Props = { value: undefined };

// OR
interface Props {
  value?: string;
}
const obj: Props = {}; // Omit instead

Supabase Queries

// ❌ WRONG
const { data } = await supabase.from('projects').select('*').eq('id', id);
return data[0];

// ✅ CORRECT
const { data } = await supabase.from('projects').select('*').eq('id', id).single();
if (!data) return null;
return data;

React Query

// ❌ WRONG
const { data } = useQuery(...);
return <div>{data.title}</div>;

// ✅ CORRECT
const { data, isLoading } = useQuery(...);
if (isLoading || !data) return <Spinner />;
return <div>{data.title}</div>;

Always run npm run typecheck before committing!