JustToThePoint English Website Version
JustToThePoint en español

TypeScript Interfaces and Data Models

TypeScript Interfaces and Data Models

Now that your tab navigation shell is working, it’s time to lay the foundation for every piece of data your app will ever touch. In this chunk we build a single file — src/db/types.ts — that defines the shape of prayers, verses, diary entries, videos, online sites, and more.

No UI, no database calls, no network requests. Just types. But those types will be used by every future post, and they will catch bugs before your app even starts.

Anime girl thinking

What you will learn

What you will build

One file — src/db/types.ts — that defines (exports) every data shape the app uses. No other code runs. No imports, no components, no side effects.

Why this post comes before everything else

Every subsequent part (database operations, API calls, UI screens) will import from types.ts. Getting the types right now means:

Test moment

npx tsc --noEmit
# Zero output = zero errors = your types are correct

Concept: TypeScript interfaces vs Python type hints

Python and TypeScript both let you describe the shape of data. Here is a side‑by‑side comparison:

Python dataclass

from dataclasses import dataclass
from typing import Optional
import datetime

@dataclass
class Prayer:
    id_myprayer: int
    title: str
    description: str
    date: str           # ISO format "YYYY-MM-DD"
    image: Optional[bytes] = None
    pages: list = None  # list of Page objects

TypeScript interface (equivalent)

interface Prayer {
  id_myprayer: number;
  title:       string;
  description: string;
  date:        string;        // ISO format "YYYY-MM-DD"
  image:       string | null; // base64 string on mobile, null if absent
  pages:       Page[];        // array of Page objects
}

Key differences

Feature Python TypeScript
Optional field Optional[str] or str = None field?: string or string | null
List of items list[Page] Page[] or Array
Number type int or float (separate) number (covers both)
Binary data bytes string (base64 encoded)
No value None null or undefined
Interface keyword dataclass / TypedDict interface or type

Why string | null instead of Optional[str]?

In TypeScript | means “or”. string | null means “this can be a string OR it can be null”. We will see this pattern constantly.

The ? shorthand (e.g., image?: string) means the field can be absent entirely (undefined), which is slightly different from being present but null.

We use | null for database fields that are explicitly stored as NULL (like an empty image) and ? for fields that might not exist at all in a partial update object.

Step 1 — Create the src/ folder structure

cd prayer-companion

# Create the directory tree
mkdir -p src/db
mkdir -p src/services
mkdir -p src/hooks
mkdir -p src/utils
mkdir -p src/components

Your project now has two main source areas:

app/        ← screens and navigation (Expo Router)
src/        ← business logic, types, services, hooks

This separation keeps the UI code (in app/) clean and the business logic (in src/) testable and reusable.

Step 2 — Create src/db/types.ts

We’ll now write the complete type definitions. Create the file and paste the content below. I’ll then explain each section.

// src/db/types.ts
//
// DATA MODELS FOR PRAYER COMPANION
// ─────────────────────────────────
// It’s purely declarative – no functions, no database calls, no UI
// – but it defines the shape of every prayer, every verse, every diary entry, every video link,
// and every piece of loading state the app will ever handle.
//
// TypeScript uses these interfaces to:
//   1. Catch bugs at compile time ("you passed a string where a number is expected")
//   2. Power editor autocomplete (type Prayer. and see all fields)
//   3. Serve as living documentation (reading this file tells you the data structure)
//
// NOTHING IN THIS FILE RUNS AT RUNTIME.
// Interfaces are erased when TypeScript compiles to JavaScript.
// They exist only for the developer and the compiler.
//
// HOW TO READ THIS FILE
// ─────────────────────
// Read it top to bottom.  Later interfaces build on earlier ones.
// The order is: primitive helpers → database models → API responses → UI state.


// ══════════════════════════════════════════════════════════════════════════════
// SECTION 1 — PRIMITIVE HELPERS
//
// Small types used as building blocks for the main interfaces.
// Defining them once prevents copy-paste errors.
// ══════════════════════════════════════════════════════════════════════════════

/* ISODate is exactly a string, but the name tells you the string must be formatted as "YYYY-MM-DD" */
export type ISODate = string;

/* Base64-encoded image string or null. SQLite stores images as binary blobs,
  but in JavaScript we work with base64 strings. **/
export type Base64Image = string | null;

/* This is the shape of every write operation (INSERT, UPDATE, DELETE). */
export interface DBResult {
  /** true if the operation succeeded, false if it failed */
  success: boolean;
  /* the primary key of the inserted or updated row
    The id? field is optional because a DELETE doesn’t produce a new ID. */
  id?: number;
  /** The error? field carries a human‑readable message when success is false. */
  error?: string;
}

// ══════════════════════════════════════════════════════════════════════════════
// SECTION 2 — DATABASE MODELS (Prayers and Pages)
//
// This section is the heart of the data layer.
// It directly mirrorsr the SQLite schema defined in Python's prayer_db.py.
// Every field name matches the column name in the database.
//
// Schema reminder:
//   myprayers: id_myprayer, title, description, date, image
//   pages:     id, myprayerid, page_number, content, image
// ══════════════════════════════════════════════════════════════════════════════

/**
 * One page within a prayer. A Page is a step inside a multi‑page prayer.
 * Maps to a row in the `pages` table.
 *
 * Pages are always fetched in page_number order.
 * page_number is 1-based (first page = 1, not 0).
 *
 */
export interface Page {
  /** SQLite autoincrement primary key in the pages table.
   * On insert we leave it out (the database assigns it), but after fetch it’s always present.
  */
  id: number;

  /** Foreign key linking to the parent prayer, myprayers.id_myprayer */
  myprayerid: number;

  /**
   * 1-based position within the prayer.
   * Page 1 = first page shown, page N = last.
   * The Python database has a UNIQUE constraint on (myprayerid, page_number)
   * so there are never duplicate page numbers.
   */
  page_number: number;

  /**
   * The prayer text for this step.
   * Can be plain text or HTML.
   */
  content: string;

  /**
   * Optional image for this specific page.
   * Stored as BLOB in SQLite, served as base64 in JavaScript.
   */
  image: Base64Image;
}

/**
 * One prayer entry — the main entity in the app.
 * Maps to a row in the `myprayers` table, with pages joined in.
 *
 * WHY `pages` is included here:
 *   The Python prayer_db.py always fetches pages alongside a prayer:
 *     return _row_to_prayer(row, pages)
 *   We do the same in TypeScript — a Prayer always carries its pages.
 *
 * When `pages` is an empty array [], the prayer exists, but has no content yet (like a newly created title).
 * When `pages` is omitted in list views, we use `PrayerSummary` instead
 * (defined below) to avoid fetching all page content unnecessarily.
 */
export interface Prayer {
  /** Primary key in the myprayers table */
  id_myprayer: number;

  /**
   * The prayer's display name, e.g., "Ignatian Morning Prayer", "Evening Examen"
   * Required — a prayer must have a title.
   */
  title: string;

  /**
   * One-sentence description shown below the title, e.g.,
   * "A five-step reflection to begin the day in God's presence."
   * Required string (empty string if missing), because the Python database defaults to "" not None.
   */
  description: string;

  /**
   * The calendar date this prayer is associated with.
   * Format: ISODate (string) "YYYY-MM-DD".
   * Used by todayPrayer() to find the prayer scheduled for today.
   * Empty string if the prayer is not date-specific (undated prayers).
   */
  date: ISODate;

  /**
   * Optional cover image shown in the prayer header.
   * Base64Image (string | null) where null means no image was stored.
   */
  image: Base64Image;

  /**
   * All pages of this prayer, ordered by page_number ascending.
   * An empty array means the prayer exists but has no content pages yet.
   */
  pages: Page[];
}

/**
 * A lightweight Prayer without page content.
 * Used in list views where we show title + date but do NOT load page content.
 *
 * Loading 200 prayers with all their pages would be slow and inefficient.
 * PrayerSummary lets us show the list instantly, then fetch the full
 * Prayer with pages only when the user taps one.
 *
 * Equivalent to Python's list_prayers() which sets pages = [].
 *
 * The `Omit<Prayer, "pages">` syntax means:
 *   "Take the Prayer interface and remove the 'pages' field."
 * So PrayerSummary has all Prayer fields (id_myprayer, title, description, date, image)
 * EXCEPT pages.
 */
export type PrayerSummary = Omit<Prayer, "pages">;

/**
 * The data required to create a new prayer.
 * All fields optional because the user might not fill everything in.
 *
 * WHY a separate type instead of Partial<Prayer>?
 *   Partial<Prayer>? would make everything optional, including id_myprayer and pages
 * – which doesn’t make sense when creating a new prayer.
 * Instead, we define exactly which fields the creation form needs.
 */
export interface NewPrayer {
  title:       string;
  /* NewPrayer: title is required (a prayer must have a name). All others are optional. */
  description?: string;
  date?:        ISODate;
  image?:       Base64Image;
}

/**
 * The data required to update an existing prayer.
 * All fields optional — you can update just the title, just the date, etc.
 *
 * Equivalent to Python's update_prayer(prayer_id, title=None, description=None, ...)
 * You pass only the fields you want to change.
 */
export interface UpdatePrayer {
  title?:       string;
  description?: string;
  date?:        ISODate;
  image?:       Base64Image;
}

/**
 * NewPage – adding a page to an existing prayer.
 * Used by addContentPrayer() function later.
 * Notice page_number is required – you can’t add a page without specifying its position.
 */
export interface NewPage {
  page_number: number;
  content:     string;
  image?:      Base64Image;
}

// ══════════════════════════════════════════════════════════════════════════════
// SECTION 3 — DIARY
//
// The Prayer Diary is file‑based, not SQLite. Each entry is a Markdown file named by date
// (e.g., 2026-05-19.md). This section defines the TypeScript shapes.
// ══════════════════════════════════════════════════════════════════════════════

/**
 * One diary entry — a single day's prayer journal.
 *
 * The content is stored as a Markdown (.md) file in the app's document
 * directory.  The filename is the date: "2026-05-14.md".
 *
 * In the Python app this was ~/dotfiles/myArch/config/bible/prayer_diary/.
 * In React Native it is FileSystem.documentDirectory + "diary/".
 */
export interface DiaryEntry {
  /**
   * The date of this entry. It is also the filename (without .md extension).
   * Format: ISODate "YYYY-MM-DD"
   */
  date: ISODate;

  /**
   * The full Markdown text content of the entry.
   * It is loaded lazily — the list view only shows the date and a preview,
   * the full content is loaded ONLY when the user opens the entry.
   */
  content: string;

  /**
   * Word count of the entry.
   * Computed on load by counting words (content.split(/\s+/).length).
   * Shows a “~250 words” badge in the list.
   */
  wordCount: number;

  /**
   * First line of content with Markdown formatting stripped, used as a preview in the entry list.
   * It strips Markdown formatting (# headings, ** bold, etc.)
   * so the list shows plain readable text.
   */
  preview: string;
}

/**
 * Lightweight version of DiaryEntry for list views.
 * DiaryEntrySummary omits the heavy content field, exactly like PrayerSummary.
 * List screens can render quickly, and the full content is fetched only when needed.
 */
export type DiaryEntrySummary = Omit<DiaryEntry, "content">;

// ══════════════════════════════════════════════════════════════════════════════
// SECTION 4 — VIDEOS TAB
//
// Data loaded from prayer_videos.txt
// ══════════════════════════════════════════════════════════════════════════════

/**
 * One entry in the prayer videos list.
 * Loaded from prayer_videos.txt (bundled in data/).
 *
 * File format (prayer_videos.txt):
 *   VIDEO_ID  Title of the video
 *   Ac_nVJASVfc  Peaceful Morning Prayer Music - 1 Hour
 *
 * In React Native, YouTube videos play directly in a WebView with the embed
 * URL: https://www.youtube.com/embed/VIDEO_ID
 * No local HTTP server workaround needed (unlike the Python/Qt app).
 */
export interface VideoSite {
  /**
   * YouTube video ID, e.g., "Ac_nVJASVfc"
   * The full embed URL is: https://www.youtube.com/embed/${id}
   */
  id: string;

  /**
   * Human-readable title shown in the video list.
   * Example: "Peaceful Morning Prayer Music - 1 Hour"
   */
  title: string;
}


// ══════════════════════════════════════════════════════════════════════════════
// SECTION 5 — ONLINE PRAYER TAB
//
// Data loaded from prayeronline.txt
// ══════════════════════════════════════════════════════════════════════════════

/**
 * One prayer website in the Online Prayer tab.
 * Loaded from prayeronline.txt (bundled in data/).
 *
 * Loaded from prayeronline.txt in a colon‑separated format:
 *   url:name:description
 *   https://www.sacredspace.com:Sacred Space:Daily Ignatian prayer...
 *
 * The site is loaded in a WebView inside the app.
 * If the site blocks embedding, a fallback button opens it in the browser.
 */
export interface PrayerSite {
  /**
   * Full URL including https://, e.g., "https://www.sacredspace.com"
   */
  url: string;

  /**
   * Short display name shown in the site list, e.g., "Sacred Space"
   */
  name: string;

  /**
   * One-sentence description of what the site offers, e.g., "Daily Ignatian prayer by the Irish Jesuits"
   */
  description: string;
}


// ══════════════════════════════════════════════════════════════════════════════
// SECTION 6 — BIBLE / VERSE TAB
//
// Data returned by verse lookup services
// ══════════════════════════════════════════════════════════════════════════════

/**
 * A Bible verse result — the output of any verse lookup function.
 * Used by the Verse tab to display the day's scripture.
 *
 * The source field tells the user WHERE this verse came from,
 * so they know if it is online or offline.
 */
export interface VerseResult {
  /**
   * The Bible reference string, e.g., "John 3:16", "Psalm 23:1-3"
   */
  ref: string;

  /**
   * The verse text.
   * May include multiple verses if the reference is a range (e.g. "John 3:16-17").
   * Plain text — no HTML formatting.
   */
  text: string;

  /**
   * source is a human‑readable string like "KJV (offline)", "OurManna.com"
   *   "YouVersion", "bible-api.com" or "KJV (offline) · Day 42 of 365"
   * so the user knows where the verse came from.
   */
  source: string;
}

/**
 * The status of a verse lookup — used by the useVerse hook (Chunk 17).
 *
 * This describes the full lifecycle of fetching a verse:
 *   idle     → initial state, nothing requested yet
 *   loading  → fetch in progress
 *   success  → verse loaded successfully
 *   error    → all sources have failed
 */
export type VerseStatus = "idle" | "loading" | "success" | "error";

/**
 * A parsed Bible reference — the output of parsing a string like "John 3:16".
 * Used internally by bibleService.ts to look up specific verses.
 *
 * Equivalent to Python's _get_book_chapter_verses() return value:
 *   tuple[str, int, list[int]]
 */
export interface ParsedReference {
  /**
   * The freebible/Sword abbreviation for the book.
   * Example: "John", "Pslm", "1 Pet"
   * Looked up from BOOK_MAP in bibleService.ts.
   */
  bookAbbrev: string;

  /** Chapter number (1-based) */
  chapter: number;

  /**
   * List of verse numbers to fetch.
   * Single verse: [16]
   * Range "3:16-17": [16, 17]
   */
  verses: number[];
}


// ══════════════════════════════════════════════════════════════════════════════
// SECTION 7 — UI STATE TYPES
//
// Types used by React hooks and components to describe loading states,
// error conditions, and pagination.
// ══════════════════════════════════════════════════════════════════════════════

/**
 * Generic async data container that almost every screen will use.
 * Used by hooks to hold data + loading + error state together.
 *
 * This is a generic interface – T can be any type.
 */
export interface AsyncState<T> {
  /** The loaded data, or null if not yet loaded */
  data:    T | null;
  /** true while the async operation is in progress */
  loading: boolean;
  /** Error message if the operation failed, null otherwise */
  error:   string | null;
}

/**
 * Pagination parameters for list queries.
 * Used by listPrayers() and diary list functions.
 */
export interface PaginationParams {
  /** How many items to return */
  limit:  number;
  /** How many items to skip (for page 2: offset = limit * 1) */
  offset: number;
}

/**
 * A paginated result set.
 * T is the item type (e.g. PrayerSummary, DiaryEntrySummary).
 */
export interface PaginatedResult<T> {
  /** The items for this page */
  items:    T[];
  /** Total number of items across all pages */
  total:    number;
  /** true if there are more items after this page */
  hasMore:  boolean;
}

/**
 * The state managed by the usePrayers hook. These interfaces document exactly what
 * a tab screen’s state looks like (what it needs to render itself).
 * The Prayers screen needs a list of summaries,
 * a currently selected prayer (with full pages), and a page index for navigation.
 */
export interface PrayersState {
  /** All prayers (summaries, no page content) */
  prayers:      PrayerSummary[];
  /** The currently selected prayer with full page content */
  current:      Prayer | null;
  /** Which page the user is on (0-based index into current.pages) */
  pageIndex:    number;
  /** true while loading from the database */
  loading:      boolean;
  /** Error message if the DB query failed */
  error:        string | null;
}

/**
 * The state managed by the useDiary hook.
 * The Diary screen adds an isDirty flag for unsaved changes.
 */
export interface DiaryState {
  /** All diary entries (summaries — no full content) */
  entries:    DiaryEntrySummary[];
  /** The currently open entry with full content */
  current:    DiaryEntry | null;
  /** true while reading/writing a file */
  loading:    boolean;
  /** Error message if a file operation failed */
  error:      string | null;
  /** true if the current entry has unsaved changes */
  isDirty:    boolean;
}

Verify with the TypeScript compiler

# After you’ve created src/db/types.ts, run the TypeScript compiler in check‑only mode:
npx tsc --noEmit

Expected output: nothing. If you’ve made a typo (like a missing export or a circular reference), tsc will report the exact line. Fix the error and run again.

Because this file has no runtime code, there’s nothing to test in the emulator. The compiler is your test suite.

Bitcoin donation

JustToThePoint Copyright © 2011 - 2026 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.