Skip to content

Last updated:

TypeScript Configuration

CPR uses TypeScript 5.x with strict mode enabled. Nuxt 4 provides full TypeScript support with auto-generated type definitions, auto-imports, and path alias resolution.

Configuration

TypeScript is configured in two places:

nuxt.config.ts

ts
// nuxt.config.ts
typescript: {
  strict: true,
  shim: false,
},
  • strict: true -- Enables all strict type-checking options (strictNullChecks, strictFunctionTypes, strictBindCallApply, noImplicitAny, etc.). This catches more bugs at compile time and is strongly recommended for a medical application where data integrity matters.
  • shim: false -- Disables the legacy shim.d.ts file for .vue imports. Nuxt 4 generates proper type definitions through its .nuxt/ directory, making the shim unnecessary.

tsconfig.json

The root tsconfig.json references Nuxt's generated TypeScript configs:

json
{
  "files": [],
  "references": [
    { "path": "./.nuxt/tsconfig.app.json" },
    { "path": "./.nuxt/tsconfig.server.json" },
    { "path": "./.nuxt/tsconfig.shared.json" },
    { "path": "./.nuxt/tsconfig.node.json" }
  ]
}

These generated configs provide:

  • Path aliases (~/ maps to app/)
  • Auto-import type definitions (so ref, computed, useRoute are typed without imports)
  • Vue SFC type support
  • Module resolution for .vue files

Type Definition Patterns

Domain Types

CPR defines domain types in app/types/. Each domain has its own type file:

ts
// app/types/patient.type.ts
export interface Patient {
  id: number;
  pin: string;
  last_name: string;
  first_name: string;
  middle_name: string;
  birthdate: string;
  sex: string;
  civil_status: string;
  email: string;
  address: string;
  photo_url: string;
  categories: PatientCategory[];
  status: string;
  created_at: string;
  updated_at: string;
}

export interface PatientCategory {
  id: string;
  name: string;
}
ts
// app/types/branch.type.ts
export interface Branch {
  id: string;
  name: string;
  address: string;
  code?: string;
  email?: string;
  is_active?: boolean;
  phone?: string;
}
ts
// app/types/user.type.ts
export interface AuthUser {
  id: number;
  name: string;
  username: string;
  email: string;
  phone?: string;
  role?: string[];
  permissions?: string[];
  default_branch?: Branch;
}

API Response Types

Standard API response wrappers are defined in app/types/apiReponse.type.ts:

ts
// Single resource response
export interface ApiResponse<T> {
  data: T;
  message: string;
}

// Paginated response (inline pagination)
export interface PaginatedReponse<T> {
  data: paginatedData<T>;
  message: string;
}

// Paginated response (Laravel Resource style)
export interface PaginatedResourceResponse<T> {
  data: T[];
  meta: MetaResponse;
  links: LinkResponse;
  message: string;
}

// Generic API request parameters
export interface GetApiParams<T> {
  search?: string;
  filters?: T;
  page?: number;
  perPage?: number;
  sortField?: string;
  sortDirection?: 'asc' | 'desc';
}

Component Props

Vue components use TypeScript interfaces for prop definitions:

ts
// From UiButton.vue
interface Props {
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  type?: 'button' | 'submit' | 'reset';
  disabled?: boolean;
  loading?: boolean;
  leftIcon?: Component;
  rightIcon?: Component;
}

const props = withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'md',
  type: 'button',
  disabled: false,
  loading: false,
  leftIcon: undefined,
  rightIcon: undefined,
});
ts
// From UiInput.vue
interface Props {
  modelValue?: string | number | null;
  label?: string;
  id?: string;
  type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'search' | 'date' | 'time';
  placeholder?: string;
  disabled?: boolean;
  leftIcon?: Component;
  rightIcon?: Component;
  size?: 'sm' | 'md' | 'lg';
  fullWidth?: boolean;
  error?: string;
  required?: boolean;
}

Emit Types

Emit declarations use typed generics:

ts
// From UiModal.vue
const emit = defineEmits<{
  'update:modelValue': [value: boolean];
  close: [];
}>();

// From UiInput.vue
const emit = defineEmits<{
  'update:modelValue': [value: string | number | null];
}>();

// From UiTable.vue
defineEmits<{
  'row-click': [row: Record<string, unknown>];
}>();

Composable Return Types

Composables use TypeScript's type inference. The return type is inferred from the returned object:

ts
// The return type is inferred automatically
export const useCreateInsurance = () => {
  const loading = ref(false);       // Ref<boolean>
  const error = ref('');            // Ref<string>
  const success = ref(false);      // Ref<boolean>

  const createInsurance = async (data: InsuranceFormData) => {
    // ...
  };

  return { loading, error, success, createInsurance };
};

Generic Composables

Some composables use generics for reusability:

ts
// useApiFetch is generic over the response type
export const useApiFetch = async <T>(
  endpoint: string,
  options: ApiFetchOptions = {}
): Promise<T> => {
  // ...
  return $fetch<T>(url, { ...options, headers });
};

// Usage with type parameter
const patients = await useApiFetch<PaginatedResourceResponse<Patient>>('/patients');
ts
// useFormValidation is generic over the form shape
export const useFormValidation = <T extends Record<string, unknown>>(
  rules: ValidationRules<T>
) => {
  // ...
};

Nuxt Auto-Imports for TypeScript

Nuxt auto-imports are fully typed. The .nuxt/ directory contains generated type definitions so that ref, computed, watch, useRoute, and other auto-imports have correct types without explicit import statements:

vue
<script setup lang="ts">
// No imports needed -- these are auto-imported with full types
const count = ref(0);           // Ref<number>
const doubled = computed(() => count.value * 2);  // ComputedRef<number>
const route = useRoute();       // RouteLocationNormalizedLoaded
</script>

Custom composables in app/composables/ are also auto-imported with types:

vue
<script setup lang="ts">
// Auto-imported from app/composables/useValidationRules.ts
const validate = required('Patient Name');  // (value: unknown) => string | null
</script>

Strict Mode Benefits

With strict: true, TypeScript catches common issues:

Null Checks

ts
// Error: Object is possibly 'null'
const name = authStore.state.user.name;

// Correct: Check for null first
const name = authStore.state.user?.name ?? 'Unknown';

No Implicit Any

ts
// Error: Parameter 'e' implicitly has an 'any' type
const handleError = (e) => { ... };

// Correct: Explicit type
const handleError = (e: FetchError) => { ... };

Exhaustive Checks

ts
// TypeScript ensures all cases are handled
type StatusColor = 'gray' | 'yellow' | 'green';

const getLabel = (color: StatusColor): string => {
  switch (color) {
    case 'gray': return 'Discharged';
    case 'yellow': return 'Pending';
    case 'green': return 'Active';
    // If a new color is added, TypeScript will error here
  }
};

Adding New Types

When adding a new domain to CPR:

  1. Create a type file: app/types/newDomain.type.ts
  2. Export the interfaces
  3. Import in composables and services: import type { MyType } from '~/types/newDomain.type'
  4. Use import type for type-only imports (tree-shaking benefit)
ts
// app/types/newDomain.type.ts
export interface NewDomainResource {
  id: string;
  name: string;
  // ...
}

export interface NewDomainFormData {
  name: string;
  // ...
}

CPR - Clinical Patient Records