Skip to content

Using React Signify with TypeScript

Overview

React Signify is built with TypeScript-first approach, providing:

  • Automatic type inference: Types are automatically inferred
  • Full type safety: Compile-time error detection
  • Excellent IntelliSense: Auto-complete and documentation
  • Zero configuration: No additional setup required

TypeScript Installation

Installing dependencies

bash
# Install TypeScript (if not already installed)
npm install -D typescript @types/react @types/react-dom

# React Signify already includes types
npm install react-signify

TypeScript config

json
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ES6"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

Type Safety with Signify

1. Basic Type Inference

tsx
import { signify } from "react-signify";

// ✅ Type automatically inferred
const sCount = signify(0); // Type: Signify<number>
const sMessage = signify("Hello"); // Type: Signify<string>
const sFlag = signify(true); // Type: Signify<boolean>

function Component() {
  const count = sCount.use(); // Type: number
  const message = sMessage.use(); // Type: string
  const flag = sFlag.use(); // Type: boolean

  return (
    <div>
      {count} - {message} - {flag}
    </div>
  );
}

2. Explicit Types

tsx
import { signify } from "react-signify";

// ✅ Explicit types for complex objects
interface User {
  id: number;
  name: string;
  email: string;
  profile?: {
    avatar?: string;
    bio?: string;
  };
}

const sUser = signify<User | null>(null);

function UserProfile() {
  const user = sUser.use(); // Type: User | null

  return (
    <div>
      {user ? (
        <div>
          <h1>{user.name}</h1> {/* ✅ TypeScript knows user is not null here */}
          <p>{user.email}</p>
          {user.profile?.avatar && (
            <img src={user.profile.avatar} alt="Avatar" />
          )}
        </div>
      ) : (
        <p>Please login</p>
      )}
    </div>
  );
}

3. Generic Types

tsx
import { signify } from "react-signify";

// ✅ Generic interfaces
interface ApiResponse<T> {
  data: T;
  status: "loading" | "success" | "error";
  error?: string;
}

interface Product {
  id: number;
  name: string;
  price: number;
}

const sProducts = signify<ApiResponse<Product[]>>({
  data: [],
  status: "loading",
});

// ✅ Type-safe updates
function loadProducts() {
  sProducts.set((prev) => {
    prev.value.status = "loading"; // ✅ Valid
    // prev.value.status = 'invalid'; // ❌ TypeScript error!
  });

  fetchProducts()
    .then((products) => {
      sProducts.set((prev) => {
        prev.value.data = products; // ✅ Type: Product[]
        prev.value.status = "success";
      });
    })
    .catch((error) => {
      sProducts.set((prev) => {
        prev.value.status = "error";
        prev.value.error = error.message;
      });
    });
}

Advanced TypeScript Patterns

1. Discriminated Unions

tsx
import { signify } from "react-signify";

// ✅ State with discriminated unions
type LoadingState = {
  type: "loading";
};

type SuccessState = {
  type: "success";
  data: User[];
};

type ErrorState = {
  type: "error";
  message: string;
};

type AsyncState = LoadingState | SuccessState | ErrorState;

const sUserList = signify<AsyncState>({ type: "loading" });

function UserList() {
  const state = sUserList.use();

  // ✅ TypeScript narrows types in switch
  switch (state.type) {
    case "loading":
      return <div>Loading...</div>;

    case "success":
      return (
        <ul>
          {state.data.map(
            (
              user // ✅ TypeScript knows state.data exists
            ) => (
              <li key={user.id}>{user.name}</li>
            )
          )}
        </ul>
      );

    case "error":
      return <div>Error: {state.message}</div>; // ✅ TypeScript knows state.message exists
  }
}

2. Type-safe Slicing

tsx
import { signify } from "react-signify";

interface ShoppingCart {
  items: CartItem[];
  discount: number;
  shipping: number;
}

interface CartItem {
  id: number;
  productId: number;
  name: string;
  price: number;
  quantity: number;
}

const sCart = signify<ShoppingCart>({
  items: [],
  discount: 0,
  shipping: 0,
});

// ✅ Type-safe slices with automatic inference
const ssItemCount = sCart.slice((cart) => cart.items.length); // Type: number
const ssSubtotal = sCart.slice((cart) =>
  cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
); // Type: number
const ssTotal = sCart.slice((cart) => {
  const subtotal = cart.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  return subtotal - cart.discount + cart.shipping;
}); // Type: number

// ✅ Complex slice with object return
const ssCartSummary = sCart.slice((cart) => ({
  itemCount: cart.items.length,
  subtotal: cart.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  ),
  total:
    cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0) -
    cart.discount +
    cart.shipping,
  isEmpty: cart.items.length === 0,
})); // Type: { itemCount: number; subtotal: number; total: number; isEmpty: boolean }

function CartSummary() {
  const summary = ssCartSummary.use();

  return (
    <div>
      <p>Items: {summary.itemCount}</p> {/* ✅ Full IntelliSense */}
      <p>Subtotal: ${summary.subtotal}</p>
      <p>Total: ${summary.total}</p>
      {summary.isEmpty && <p>Cart is empty</p>}
    </div>
  );
}

3. Action Creators with Types

tsx
import { signify } from "react-signify";

// ✅ Typed action creators
interface Todo {
  id: number;
  text: string;
  completed: boolean;
  priority: "low" | "medium" | "high";
}

const sTodos = signify<Todo[]>([]);

// ✅ Type-safe action creators
export const todoActions = {
  add: (text: string, priority: Todo["priority"] = "medium") => {
    sTodos.set((prev) => {
      prev.value.push({
        id: Date.now(),
        text,
        completed: false,
        priority,
      });
    });
  },

  toggle: (id: number) => {
    sTodos.set((prev) => {
      const todo = prev.value.find((t) => t.id === id);
      if (todo) {
        todo.completed = !todo.completed;
      }
    });
  },

  updatePriority: (id: number, priority: Todo["priority"]) => {
    sTodos.set((prev) => {
      const todo = prev.value.find((t) => t.id === id);
      if (todo) {
        todo.priority = priority; // ✅ Type-safe
      }
    });
  },

  remove: (id: number) => {
    sTodos.set((prev) => {
      prev.value = prev.value.filter((t) => t.id !== id);
    });
  },
};

// ✅ Usage with full type safety
function TodoApp() {
  const todos = sTodos.use();

  const handleAddTodo = (text: string) => {
    todoActions.add(text, "high"); // ✅ IntelliSense for priority options
  };

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>
          <span>{todo.text}</span>
          <button onClick={() => todoActions.toggle(todo.id)}>
            {todo.completed ? "Undo" : "Complete"}
          </button>
          <select
            value={todo.priority}
            onChange={(e) =>
              todoActions.updatePriority(
                todo.id,
                e.target.value as Todo["priority"]
              )
            }
          >
            <option value="low">Low</option>
            <option value="medium">Medium</option>
            <option value="high">High</option>
          </select>
        </div>
      ))}
    </div>
  );
}

Configuration Types

1. Signify Configuration

tsx
import { signify, type TSignifyConfig } from "react-signify";

// ✅ Typed configuration
const config: TSignifyConfig = {
  cache: {
    key: "user-preferences",
    type: "LocalStorage", // Optional: 'LocalStorage' | 'SesionStorage'
  },
  syncKey: "user-prefs-sync",
};

interface UserPreferences {
  theme: "light" | "dark";
  language: "en" | "vi" | "fr";
  notifications: boolean;
}

const sUserPrefs = signify<UserPreferences>(
  {
    theme: "light",
    language: "en",
    notifications: true,
  },
  config
);

2. Cache Configuration

tsx
import { signify, type TCacheInfo } from "react-signify";

// ✅ Typed cache configuration
const cacheConfig: TCacheInfo = {
  key: "api-cache",
  type: "LocalStorage", // Optional: defaults to memory cache if not specified
};

const sApiData = signify([], { cache: cacheConfig });

Common Type Patterns

1. Optional Properties

tsx
import { signify } from 'react-signify';

interface UserForm {
  // Required fields
  email: string;
  password: string;

  // Optional fields
  firstName?: string;
  lastName?: string;
  phone?: string;
}

const sUserForm = signify<UserForm>({
  email: '',
  password: ''
  // Optional fields can be omitted in initial value
});

function UserFormComponent() {
  const form = sUserForm.use();

  const updateField = <K extends keyof UserForm>(
    field: K,
    value: UserForm[K]
  ) => {
    sUserForm.set(prev => {
      prev.value[field] = value; // ✅ Type-safe field updates
    });
  };

  return (
    <form>
      <input
        value={form.email}
        onChange={(e) => updateField('email', e.target.value)}
      />
      <input
        value={form.firstName || ''} {/* ✅ Handle optional fields */}
        onChange={(e) => updateField('firstName', e.target.value)}
      />
    </form>
  );
}

2. Enum Types

tsx
import { signify } from "react-signify";

// ✅ Enum for type safety
enum OrderStatus {
  PENDING = "pending",
  PROCESSING = "processing",
  SHIPPED = "shipped",
  DELIVERED = "delivered",
  CANCELLED = "cancelled",
}

interface Order {
  id: number;
  status: OrderStatus;
  items: OrderItem[];
  total: number;
}

const sCurrentOrder = signify<Order | null>(null);

// ✅ Type-safe status updates
function updateOrderStatus(newStatus: OrderStatus) {
  sCurrentOrder.set((prev) => {
    if (prev.value) {
      prev.value.status = newStatus; // ✅ Only valid enum values
    }
  });
}

function OrderStatusComponent() {
  const order = sCurrentOrder.use();

  if (!order) return <div>No order</div>;

  return (
    <div>
      <p>Status: {order.status}</p>
      <select
        value={order.status}
        onChange={(e) => updateOrderStatus(e.target.value as OrderStatus)}
      >
        {Object.values(OrderStatus).map((status) => (
          <option key={status} value={status}>
            {status.charAt(0).toUpperCase() + status.slice(1)}
          </option>
        ))}
      </select>
    </div>
  );
}

3. Generic Utilities

tsx
import { signify } from "react-signify";

// ✅ Generic utility types
type AsyncData<T> = {
  data: T | null;
  loading: boolean;
  error: string | null;
};

// ✅ Helper function with generics
function createAsyncSignify<T>(initialData: T | null = null) {
  return signify<AsyncData<T>>({
    data: initialData,
    loading: false,
    error: null,
  });
}

// ✅ Usage
interface User {
  id: number;
  name: string;
  email: string;
}

const sUsers = createAsyncSignify<User[]>([]);
const sCurrentUser = createAsyncSignify<User>();

// ✅ Type-safe async operations
async function loadUsers() {
  sUsers.set((prev) => {
    prev.value.loading = true;
    prev.value.error = null;
  });

  try {
    const users = await fetchUsers(); // Returns User[]
    sUsers.set((prev) => {
      prev.value.data = users;
      prev.value.loading = false;
    });
  } catch (error) {
    sUsers.set((prev) => {
      prev.value.error = error.message;
      prev.value.loading = false;
    });
  }
}

TypeScript Best Practices

1. Strict Type Checking

tsx
// ✅ Enable strict mode in tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true
  }
}

// ✅ Use strict types in code
interface StrictUser {
  readonly id: number;        // Immutable ID
  name: string;               // Required
  email: string;              // Required
  avatar?: string;            // Optional
  roles: readonly string[];   // Immutable array
}

const sUser = signify<StrictUser | null>(null);

2. Type Guards

tsx
import { signify } from "react-signify";

// ✅ Type guards for runtime safety
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    "email" in value
  );
}

const sUserData = signify<unknown>(null);

function UserComponent() {
  const userData = sUserData.use();

  if (!isUser(userData)) {
    return <div>Invalid user data</div>;
  }

  // ✅ TypeScript knows userData is User type
  return (
    <div>
      <h1>{userData.name}</h1>
      <p>{userData.email}</p>
    </div>
  );
}

3. Conditional Types

tsx
import { signify } from "react-signify";

// ✅ Conditional types for advanced patterns
type SignifyState<T> = T extends string | number
  ? T
  : T extends object
  ? T
  : never;

type UIState = {
  theme: "light" | "dark";
  sidebar: boolean;
  modal: string | null;
};

const sUIState = signify<SignifyState<UIState>>({
  theme: "light",
  sidebar: false,
  modal: null,
});

// ✅ Conditional slice types
type SliceReturnType<T, K extends keyof T> = T[K];

function createFieldSlice<T, K extends keyof T>(
  signify: ReturnType<typeof signify<T>>,
  field: K
) {
  return signify.slice((state) => state[field]) as {
    use(): SliceReturnType<T, K>;
    value: SliceReturnType<T, K>;
    // ... other slice methods
  };
}

const ssTheme = createFieldSlice(sUIState, "theme"); // Type: 'light' | 'dark'

Debugging TypeScript

1. Type Inspection

tsx
import { signify } from "react-signify";

const sData = signify({ count: 0, message: "hello" });

// ✅ Inspect types in development
type DataType = typeof sData.value; // { count: number; message: string }
type UseReturnType = ReturnType<typeof sData.use>; // { count: number; message: string }

// ✅ Use type assertion when needed
const ssSpecificSlice = sData.slice((d) => d.count) as any as {
  use(): number;
  value: number;
};

2. Common TypeScript Errors

tsx
// ❌ Error: Type mismatch
const sCount = signify(0);
// sCount.set('hello'); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

// ✅ Fix: Correct type
sCount.set(42);

// ❌ Error: Undefined property
const sUser = signify<{ name: string } | null>(null);
// const name = sUser.use().name; // Error: Object is possibly 'null'

// ✅ Fix: Null check
const user = sUser.use();
const name = user?.name || "Guest";

// ❌ Error: Incorrect slice type
const sItems = signify<{ id: number; name: string }[]>([]);
// const firstItem = sItems.slice(items => items[0].name); // Error: Object is possibly 'undefined'

// ✅ Fix: Safe access
const ssFirstItemName = sItems.slice((items) => items[0]?.name || "Unknown");

Performance with TypeScript

1. Type-only Imports

tsx
// ✅ Import only types to avoid runtime overhead
import type { User, UserPreferences } from "./types";
import { signify } from "react-signify";

const sUser = signify<User | null>(null);
const sPrefs = signify<UserPreferences>({} as UserPreferences);

2. Const Assertions

tsx
// ✅ Const assertions for better inference
const themes = ["light", "dark", "auto"] as const;
type Theme = (typeof themes)[number]; // 'light' | 'dark' | 'auto'

const sTheme = signify<Theme>("light");

// ✅ Object const assertion
const defaultUser = {
  name: "",
  email: "",
  role: "user",
} as const;

type UserRole = typeof defaultUser.role; // 'user'

Summary

TypeScript with React Signify provides:

  1. Automatic type inference: No need to declare types for most cases
  2. Compile-time safety: Catch errors before runtime
  3. Excellent developer experience: IntelliSense, auto-complete
  4. Advanced patterns: Generics, discriminated unions, conditional types
  5. Performance: Type-only imports, const assertions

Key takeaways:

  • ✅ Use explicit types for complex objects
  • ✅ Leverage slice types for computed values
  • ✅ Type guards for runtime safety
  • ✅ Strict TypeScript configuration
  • ✅ Proper null/undefined handling

Next, explore API Reference to learn about all methods and options.


💡 Tip: Start with basic types, then gradually use advanced patterns when needed.

Released under the MIT License.