React Foundation in Theme Context Intermediate 15 min read

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 export

Core Product Types

Start with the fundamental product structures:

src/types/product.ts
/**
* 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:

src/types/cart.ts
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:

src/types/customer.ts
/**
* 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:

src/types/shopify.ts
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 types
export 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:

src/types/global.d.ts
/*
* 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 scope
declare 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:

src/types/index.ts
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:

src/components/product/ProductForm.tsx
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:

src/types/guards.ts
/*
* 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:

src/types/utils.ts
/**
* 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:

src/types/api.ts
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

  1. Keep types close to Shopify’s structure: Match Liquid’s object properties as closely as possible

  2. Use camelCase: Convert Liquid’s snake_case to JavaScript’s camelCase in your serialization

  3. Document nullable fields: Shopify often returns null - type accordingly

  4. Version your types: When Shopify updates their API, update your types

  5. Don’t over-type: Only type what you actually use

Key Takeaways

  1. Organize by domain: Products, cart, customer, shop in separate files
  2. Complete but not excessive: Type what you use, skip what you don’t
  3. Window augmentation: Extend global Window for Shopify data access
  4. Type guards: Runtime validation with type narrowing
  5. Utility types: DRY with reusable type helpers
  6. API types: Type your fetch responses for end-to-end safety
  7. 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...