TypeScript Types for Shopify Objects
Build a comprehensive TypeScript type library for Shopify's data objects. Define types for products, variants, cart, customers, and more.
Type safety is one of the biggest benefits of using TypeScript in your Shopify theme. In this lesson, we’ll build a complete type library for Shopify’s data objects that you’ll use throughout your React components.
Setting Up Your Types Directory
Create a dedicated folder for Shopify types:
src/types/├── shopify.ts # Core Shopify object types├── cart.ts # Cart-specific types├── product.ts # Product-specific types├── customer.ts # Customer types├── global.d.ts # Window augmentation└── index.ts # Barrel exportCore Product Types
Start with the fundamental product structures:
/** * A product image from Shopify */export interface ProductImage { id: number; src: string; alt: string | null; width: number; height: number; position: number;}
/** * Image with responsive srcset for different sizes */export interface ResponsiveImage extends ProductImage { srcset: { small: string; // 400px medium: string; // 800px large: string; // 1200px };}
/** * A product option (like "Size" or "Color") */export interface ProductOption { name: string; position: number; values: string[];}
/** * A single product variant */export interface ProductVariant { id: number; title: string; price: number; compareAtPrice: number | null; available: boolean; sku: string | null; barcode: string | null; weight: number; weightUnit: string; options: string[]; optionValues: Record<string, string>; image: ProductImage | null; inventoryQuantity: number | null; inventoryPolicy: 'deny' | 'continue';}
/** * Complete product data */export interface Product { id: number; title: string; handle: string; description: string; descriptionHtml: string; vendor: string; type: string; tags: string[]; available: boolean; price: number; priceMin: number; priceMax: number; priceVaries: boolean; compareAtPrice: number | null; compareAtPriceMin: number | null; compareAtPriceMax: number | null; images: ProductImage[]; featuredImage: ProductImage | null; variants: ProductVariant[]; options: ProductOption[]; selectedVariantId: number | null; url: string;}
/** * Minimal product data for cards/grids */export interface ProductCard { id: number; title: string; handle: string; vendor: string; price: number; compareAtPrice: number | null; available: boolean; featuredImage: ProductImage | null; url: string;}Cart Types
Define the cart and line item structures:
import type { ProductImage } from './product';
/** * Custom properties on a line item */export type LineItemProperties = Record<string, string>;
/** * A single item in the cart */export interface CartLineItem { key: string; productId: number; variantId: number; title: string; variantTitle: string | null; quantity: number; price: number; originalPrice: number; finalPrice: number; linePrice: number; finalLinePrice: number; discountedPrice: number; image: string | null; url: string; handle: string; sku: string | null; vendor: string; properties: LineItemProperties; giftCard: boolean; discountAllocations: DiscountAllocation[];}
/** * Discount applied to a line item */export interface DiscountAllocation { amount: number; discountApplication: { type: 'automatic' | 'discount_code' | 'manual' | 'script'; title: string; };}
/** * Complete cart data */export interface Cart { token: string; note: string | null; attributes: Record<string, string>; itemCount: number; totalPrice: number; totalDiscount: number; totalWeight: number; originalTotalPrice: number; items: CartLineItem[]; requiresShipping: boolean; currency: string; currencyCode: string;}
/** * Request to add item to cart */export interface AddToCartRequest { id: number; quantity: number; properties?: LineItemProperties;}
/** * Request to update cart item */export interface UpdateCartRequest { id: string; // line item key quantity: number;}
/** * Cart API response after modification */export interface CartApiResponse extends Cart { // API response is the same as cart}Customer Types
For logged-in customer data:
/** * Customer address */export interface CustomerAddress { id: number; firstName: string; lastName: string; company: string | null; address1: string; address2: string | null; city: string; province: string; provinceCode: string; country: string; countryCode: string; zip: string; phone: string | null; default: boolean;}
/** * Order line item */export interface OrderLineItem { title: string; quantity: number; price: number; sku: string | null; variantTitle: string | null; image: string | null;}
/** * Customer order */export interface CustomerOrder { id: number; name: string; orderNumber: number; createdAt: string; financialStatus: 'pending' | 'paid' | 'refunded' | 'voided'; fulfillmentStatus: 'unfulfilled' | 'partial' | 'fulfilled'; totalPrice: number; lineItems: OrderLineItem[]; shippingAddress: CustomerAddress | null; billingAddress: CustomerAddress | null;}
/** * Logged-in customer data */export interface Customer { id: number; email: string; firstName: string; lastName: string; name: string; phone: string | null; acceptsMarketing: boolean; ordersCount: number; totalSpent: number; tags: string[]; defaultAddress: CustomerAddress | null; addresses: CustomerAddress[];}Shop and Configuration Types
Global shop settings and configuration:
import type { Product, ProductCard, ProductVariant, ProductOption, ProductImage } from './product';import type { Cart, CartLineItem, AddToCartRequest, UpdateCartRequest } from './cart';import type { Customer, CustomerAddress, CustomerOrder } from './customer';
// Re-export all typesexport type { Product, ProductCard, ProductVariant, ProductOption, ProductImage, Cart, CartLineItem, AddToCartRequest, UpdateCartRequest, Customer, CustomerAddress, CustomerOrder,};
/** * Shop configuration */export interface Shop { name: string; url: string; domain: string; currency: string; currencyCode: string; moneyFormat: string; moneyWithCurrencyFormat: string; locale: string;}
/** * Current locale settings */export interface Locale { code: string; rootUrl: string; primary: boolean;}
/** * Current page context */export interface PageContext { template: string; suffix: string;}
/** * Available routes for API calls */export interface Routes { cart: string; cartAdd: string; cartChange: string; cartUpdate: string; cartClear: string; search: string; predictiveSearch: string;}
/** * Cart summary for header display */export interface CartSummary { itemCount: number; totalPrice: number; currency: string;}
/** * Navigation menu link */export interface MenuLink { title: string; url: string; active: boolean; childLinks: MenuLink[]; levels: number;}
/** * Navigation menu */export interface Menu { handle: string; title: string; levels: number; links: MenuLink[];}Window Augmentation
Extend the global Window interface for Shopify data:
/* * Window Augmentation: Tell TypeScript about global variables * * Liquid themes often expose data on window (window.Shopify, etc.) * Without this file, TypeScript errors: "Property 'Shopify' does not exist on Window" * This file extends the Window interface to include our custom properties. */
import type { Shop, Locale, PageContext, Routes, CartSummary, Customer } from './shopify';
/** * Global Shopify object structure * Matches what's typically exposed by Liquid via window.Shopify */interface ShopifyGlobal { shop: Shop; locale: Locale; page: PageContext; routes: Routes; cart: CartSummary; // Quick cart summary (count, total) customer: Customer | null; // null if not logged in}
/** * Theme-specific methods we expose globally * Allows non-React code to interact with our React components */interface ReactThemeMethods { mount: (elementId: string, config: unknown) => void; unmount: (elementId: string) => void; mountAll: () => void;}
// 'declare global' extends the global scopedeclare global { interface Window { // Optional because it might not exist until Liquid renders it Shopify?: ShopifyGlobal; ReactTheme?: ReactThemeMethods; }}
// Empty export makes this a module (required for 'declare global' to work)export {};Barrel Export
Create a single entry point for all types:
export type { // Product types Product, ProductCard, ProductVariant, ProductOption, ProductImage, ResponsiveImage,
// Cart types Cart, CartLineItem, LineItemProperties, DiscountAllocation, AddToCartRequest, UpdateCartRequest, CartApiResponse,
// Customer types Customer, CustomerAddress, CustomerOrder, OrderLineItem,
// Shop types Shop, Locale, PageContext, Routes, CartSummary, Menu, MenuLink,} from './shopify';Using Types in Components
Import and use types in your components:
import type { Product, ProductVariant } from '@/types';
interface ProductFormProps { productId: number;}
export function ProductForm({ productId }: ProductFormProps) { const [product, setProduct] = useState<Product | null>(null); const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
// Type-safe access to product properties if (!product) return null;
return ( <div> <h1>{product.title}</h1> <p>{product.vendor}</p> {/* ... */} </div> );}Type Guards
Create type guards for runtime type checking:
/* * Type Guards: Runtime type checking with TypeScript narrowing * * Problem: Data from Liquid (JSON.parse) is typed as 'unknown' or 'any' * Solution: Type guards check the shape at runtime and tell TypeScript * "after this check, you can trust this is a Product" */
import type { Product, ProductVariant, Cart, Customer } from './shopify';
/** * Checks if a value is a valid Product * The return type `value is Product` is a type predicate - it narrows the type */export function isProduct(value: unknown): value is Product { // First check: is it an object at all? if (!value || typeof value !== 'object') return false;
// Cast to access properties (safe because we'll validate them) const product = value as Record<string, unknown>;
// Check for required properties with correct types // Add more checks for stricter validation return ( typeof product.id === 'number' && typeof product.title === 'string' && typeof product.handle === 'string' && Array.isArray(product.variants) );}
/** * Checks if a value is a valid ProductVariant */export function isProductVariant(value: unknown): value is ProductVariant { if (!value || typeof value !== 'object') return false;
const variant = value as Record<string, unknown>; return ( typeof variant.id === 'number' && typeof variant.title === 'string' && typeof variant.price === 'number' && typeof variant.available === 'boolean' );}
/** * Checks if a value is a valid Cart */export function isCart(value: unknown): value is Cart { if (!value || typeof value !== 'object') return false;
const cart = value as Record<string, unknown>; return ( typeof cart.token === 'string' && typeof cart.itemCount === 'number' && Array.isArray(cart.items) );}
/** * Checks if customer is logged in * Simple but useful - avoids null/undefined checks everywhere */export function isLoggedIn(customer: Customer | null | undefined): customer is Customer { return customer !== null && customer !== undefined;}Using type guards:
import { isProduct } from '@/types/guards';import { bridge } from '@/utils/data-bridge';
function ProductForm() { const data = bridge.getProduct();
// Type guard narrows the type if (!isProduct(data)) { console.error('Invalid product data'); return <ErrorMessage />; }
// Now `data` is typed as Product return <Form product={data} />;}Utility Types
Create utility types for common patterns:
/** * Makes specific properties optional */export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
/** * Makes specific properties required */export type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
/** * Extracts the type of array elements */export type ArrayElement<T extends readonly unknown[]> = T extends readonly (infer E)[] ? E : never;
/** * Makes all properties nullable */export type Nullable<T> = { [K in keyof T]: T[K] | null };
/** * Price in cents (for clarity) */export type PriceCents = number;
/** * Shopify ID (always a number) */export type ShopifyId = number;
/** * Shopify handle (URL-safe string) */export type ShopifyHandle = string;API Response Types
Type your API calls:
import type { Cart, Product } from './shopify';
/** * Generic API response wrapper */export interface ApiResponse<T> { data: T | null; error: string | null; status: number;}
/** * Cart API endpoints responses */export interface CartAddResponse extends Cart {}export interface CartUpdateResponse extends Cart {}export interface CartRemoveResponse extends Cart {}
/** * Predictive search response */export interface PredictiveSearchResponse { resources: { results: { products: ProductSearchResult[]; collections: CollectionSearchResult[]; articles: ArticleSearchResult[]; pages: PageSearchResult[]; }; };}
export interface ProductSearchResult { id: number; title: string; handle: string; url: string; image: string | null; price: number; compareAtPrice: number | null;}
export interface CollectionSearchResult { id: number; title: string; handle: string; url: string;}
export interface ArticleSearchResult { id: number; title: string; handle: string; url: string; publishedAt: string;}
export interface PageSearchResult { id: number; title: string; handle: string; url: string;}Tips for Maintaining Types
-
Keep types close to Shopify’s structure: Match Liquid’s object properties as closely as possible
-
Use camelCase: Convert Liquid’s snake_case to JavaScript’s camelCase in your serialization
-
Document nullable fields: Shopify often returns
null- type accordingly -
Version your types: When Shopify updates their API, update your types
-
Don’t over-type: Only type what you actually use
Key Takeaways
- Organize by domain: Products, cart, customer, shop in separate files
- Complete but not excessive: Type what you use, skip what you don’t
- Window augmentation: Extend global Window for Shopify data access
- Type guards: Runtime validation with type narrowing
- Utility types: DRY with reusable type helpers
- API types: Type your fetch responses for end-to-end safety
- Barrel exports: Single import point for all types
In the next lesson, we’ll build reusable UI primitives that form the foundation of your component library.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...