TypeScript Usage Guide
This guide covers comprehensive TypeScript usage with the dcupl SDK, including type definitions, generics, type inference, and advanced typing patterns.
Installation
The dcupl SDK includes built-in TypeScript definitions:
npm install @dcupl/core @dcupl/commonyarn add @dcupl/core @dcupl/commonpnpm add @dcupl/core @dcupl/commontsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true
}
}Basic Type Usage
Importing Types
import type { ModelDefinition, PropertyDefinition, IItem, IQuery, DcuplList } from '@dcupl/common';
import { Dcupl } from '@dcupl/core';Type-Safe Model Definition
import type { ModelDefinition } from '@dcupl/common';
const productModel: ModelDefinition = {
key: 'Product',
properties: [
{ key: 'name', type: 'string' },
{ key: 'price', type: 'float' },
{ key: 'category', type: 'string' },
{ key: 'inStock', type: 'boolean' },
{ key: 'tags', type: 'Array<string>' },
],
};
// TypeScript will catch errors
const invalidModel: ModelDefinition = {
key: 'Invalid',
properties: [
// Error: 'type' is required
{ key: 'name' },
// Error: invalid type
{ key: 'price', type: 'invalid' },
],
};Custom Type Definitions
Creating Item Interfaces
Define TypeScript interfaces that match your models:
import type { IItem } from '@dcupl/common';
// Base item interface extends IItem
interface Product extends IItem {
name: string;
sku: string;
price: number;
category: string;
brand: string;
description: string;
inStock: boolean;
rating: number;
tags: string[];
createdAt: Date;
updatedAt: Date;
}
interface Category extends IItem {
name: string;
slug: string;
description: string;
parentId: string | null;
}
interface Customer extends IItem {
email: string;
firstName: string;
lastName: string;
phone: string;
address: Address;
}
interface Address {
street: string;
city: string;
state: string;
zipCode: string;
country: string;
}Complex Nested Types
interface Order extends IItem {
orderId: string;
customerId: string;
items: OrderItem[];
total: number;
status: OrderStatus;
shippingAddress: Address;
billingAddress: Address;
createdAt: Date;
updatedAt: Date;
}
interface OrderItem {
productId: string;
quantity: number;
price: number;
discount?: number;
}
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';Generic Type Usage
DcuplList with Generics
Use generics to get type-safe query results:
import { Dcupl } from '@dcupl/core';
import type { DcuplList } from '@dcupl/common';
const dcupl = new Dcupl();
// Create type-safe list
const productList: DcuplList = dcupl.lists.create({ modelKey: 'Product' });
// Execute returns typed results
const products = productList.catalog.query.items() as Product[];
// TypeScript knows the shape
products.forEach((product) => {
console.log(product.name); // ✓ TypeScript knows 'name' exists
console.log(product.price); // ✓ TypeScript knows 'price' is a number
console.log(product.invalid); // ✗ Error: Property doesn't exist
});Type-Safe Query Functions
Create reusable type-safe query functions:
function queryProducts(
list: DcuplList,
filters?: {
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
}
): Product[] {
// Reset query
list.catalog.query.clear();
// Apply filters
if (filters?.category) {
list.catalog.query.addCondition({
operator: 'eq',
attribute: 'category',
value: filters.category,
});
}
if (filters?.minPrice !== undefined) {
list.catalog.query.addCondition(
{
operator: 'gte',
attribute: 'price',
value: filters.minPrice,
},
{ mode: 'add' }
);
}
if (filters?.maxPrice !== undefined) {
list.catalog.query.addCondition(
{
operator: 'lte',
attribute: 'price',
value: filters.maxPrice,
},
{ mode: 'add' }
);
}
if (filters?.inStock !== undefined) {
list.catalog.query.addCondition(
{
operator: 'eq',
attribute: 'inStock',
value: filters.inStock,
},
{ mode: 'add' }
);
}
// Return typed results
return list.catalog.query.items() as Product[];
}
// Usage with type safety
const electronics = queryProducts(productList, { category: 'Electronics' });
const affordable = queryProducts(productList, { maxPrice: 100 });
const available = queryProducts(productList, { inStock: true });Generic Repository Pattern
class DcuplRepository<T extends IItem> {
private list: DcuplList;
constructor(
private dcupl: Dcupl,
private modelKey: string
) {
this.list = dcupl.lists.create({ modelKey });
}
findAll(): T[] {
this.list.catalog.query.clear();
return this.list.catalog.query.items() as T[];
}
findById(id: string): T | undefined {
this.list.catalog.query.clear();
this.list.catalog.query.addCondition({
operator: 'eq',
attribute: '$.value',
value: id,
});
return this.list.catalog.query.first() as T | undefined;
}
findByAttribute(attribute: keyof T, value: any): T[] {
this.list.catalog.query.clear();
this.list.catalog.query.addCondition({
operator: 'eq',
attribute: attribute as string,
value,
});
return this.list.catalog.query.items() as T[];
}
findWhere(predicate: (item: T) => boolean): T[] {
const allItems = this.findAll();
return allItems.filter(predicate);
}
count(): number {
return this.list.catalog.query.count();
}
}
// Usage
const productRepo = new DcuplRepository<Product>(dcupl, 'Product');
const allProducts = productRepo.findAll(); // Type: Product[]
const product = productRepo.findById('p1'); // Type: Product | undefined
const electronics = productRepo.findByAttribute('category', 'Electronics'); // Type: Product[]
const expensive = productRepo.findWhere((p) => p.price > 1000); // Type: Product[]Type Inference with execute()
The execute() method can infer types:
// Explicit typing
const products1: Product[] = productList.catalog.query.items() as Product[];
// Type assertion
const products2 = productList.catalog.query.items() as Product[];
// With generic function
function getProducts<T extends IItem>(list: DcuplList): T[] {
return list.catalog.query.items() as T[];
}
const products3 = getProducts<Product>(productList); // Type: Product[]Type-Safe Queries
Query Builder with Types
type QueryOperator = 'eq' | 'find' | 'gt' | 'gte' | 'lt' | 'lte' | 'typeof' | 'isTruthy' | 'size';
interface TypedQuery<T> {
attribute: keyof T;
operator: QueryOperator;
value: any;
}
class TypedQueryBuilder<T extends IItem> {
private queries: TypedQuery<T>[] = [];
constructor(private list: DcuplList) {}
where(attribute: keyof T, operator: QueryOperator, value: any): this {
this.queries.push({ attribute, operator, value });
return this;
}
execute(): T[] {
this.list.catalog.query.clear();
this.queries.forEach((query) => {
this.list.catalog.query.addCondition(
{
operator: query.operator,
attribute: query.attribute as string,
value: query.value,
},
{ mode: 'add' }
);
});
return this.list.catalog.query.items() as T[];
}
reset(): this {
this.queries = [];
this.list.catalog.query.clear();
return this;
}
}
// Usage with type safety
const builder = new TypedQueryBuilder<Product>(productList);
const results = builder
.where('category', 'eq', 'Electronics') // ✓ 'category' is valid
.where('price', 'gte', 100) // ✓ 'price' is valid
.where('inStock', 'eq', true) // ✓ 'inStock' is valid
.where('invalid', 'eq', 'value') // ✗ Error: 'invalid' doesn't exist
.execute();Strongly-Typed Filter Helpers
type FilterValue<T, K extends keyof T> = T[K];
interface FilterConfig<T extends IItem> {
eq?<K extends keyof T>(attribute: K, value: FilterValue<T, K>): void;
find?<K extends keyof T>(attribute: K, value: string): void;
gt?<K extends keyof T>(attribute: K, value: FilterValue<T, K>): void;
gte?<K extends keyof T>(attribute: K, value: FilterValue<T, K>): void;
lt?<K extends keyof T>(attribute: K, value: FilterValue<T, K>): void;
lte?<K extends keyof T>(attribute: K, value: FilterValue<T, K>): void;
isTruthy?<K extends keyof T>(attribute: K, value: boolean): void;
}
function createFilter<T extends IItem>(list: DcuplList): FilterConfig<T> {
const applyFilter = (operator: QueryOperator, attribute: string, value: any) => {
list.catalog.query.addCondition({ operator, attribute, value }, { mode: 'add' });
};
return {
eq: (attribute, value) => applyFilter('eq', attribute as string, value),
find: (attribute, value) => applyFilter('find', attribute as string, value),
gt: (attribute, value) => applyFilter('gt', attribute as string, value),
gte: (attribute, value) => applyFilter('gte', attribute as string, value),
lt: (attribute, value) => applyFilter('lt', attribute as string, value),
lte: (attribute, value) => applyFilter('lte', attribute as string, value),
isTruthy: (attribute, value) => applyFilter('isTruthy', attribute as string, value),
};
}
// Usage
const filter = createFilter<Product>(productList);
filter.eq('category', 'Electronics'); // ✓ Type-safe
filter.gte('price', 100); // ✓ Type-safe
filter.eq('inStock', true); // ✓ Type-safe
filter.find('name', 'laptop'); // ✓ Type-safe text search
filter.eq('invalid', 'value'); // ✗ Error: Property doesn't exist
filter.eq('price', 'not-a-number'); // ✗ Error: Wrong typeCustom Type Guards
Create type guards for runtime type checking:
function isProduct(item: IItem): item is Product {
return (
'name' in item &&
'price' in item &&
'category' in item &&
typeof item.name === 'string' &&
typeof item.price === 'number' &&
typeof item.category === 'string'
);
}
function isCategory(item: IItem): item is Category {
return (
'name' in item &&
'slug' in item &&
typeof item.name === 'string' &&
typeof item.slug === 'string'
);
}
// Usage
const items = list.catalog.query.items();
items.forEach((item) => {
if (isProduct(item)) {
// TypeScript knows item is Product
console.log(item.price);
} else if (isCategory(item)) {
// TypeScript knows item is Category
console.log(item.slug);
}
});Advanced Typing Patterns
Mapped Types for Queries
type QueryableFields<T> = {
[K in keyof T]: T[K] extends string | number | boolean ? K : never;
}[keyof T];
type QueryableProduct = QueryableFields<Product>;
// Type: 'name' | 'sku' | 'price' | 'category' | 'brand' | 'inStock' | 'rating'
function queryByField<T extends IItem, K extends QueryableFields<T>>(
list: DcuplList,
field: K,
value: T[K]
): T[] {
list.catalog.query.clear();
list.catalog.query.addCondition({
operator: 'eq',
attribute: field as string,
value,
});
return list.catalog.query.items() as T[];
}
// Usage
const byName = queryByField<Product, 'name'>(productList, 'name', 'Laptop');
const byPrice = queryByField<Product, 'price'>(productList, 'price', 999);Conditional Types for Filters
type FilterOperatorForType<T> = T extends string
? 'eq' | 'find' | 'typeof' | 'isTruthy' | 'size'
: T extends number
? 'eq' | 'gt' | 'gte' | 'lt' | 'lte' | 'typeof'
: T extends boolean
? 'eq' | 'isTruthy'
: 'eq' | 'typeof' | 'isTruthy';
interface TypedFilter<T extends IItem, K extends keyof T> {
attribute: K;
operator: FilterOperatorForType<T[K]>;
value: T[K] | T[K][];
}
function applyTypedFilter<T extends IItem, K extends keyof T>(
list: DcuplList,
filter: TypedFilter<T, K>
): T[] {
list.catalog.query.addCondition({
// Cast needed: conditional type not narrowed at runtime
operator: filter.operator as QueryOperator,
attribute: filter.attribute as string,
value: filter.value,
});
return list.catalog.query.items() as T[];
}
// Usage - TypeScript enforces valid operators per type
const stringFilter: TypedFilter<Product, 'name'> = {
attribute: 'name',
operator: 'find', // ✓ Valid for string
value: 'Laptop',
};
const numberFilter: TypedFilter<Product, 'price'> = {
attribute: 'price',
operator: 'gte', // ✓ Valid for number
value: 100,
};
const invalidFilter: TypedFilter<Product, 'price'> = {
attribute: 'price',
operator: 'find', // ✗ Error: 'find' not valid for number
value: 100,
};Utility Types for Facets
type FacetableFields<T> = {
[K in keyof T]: T[K] extends string | number | boolean ? K : never;
}[keyof T];
interface FacetResult<T, K extends keyof T> {
attribute: K;
values: Array<{
value: T[K];
count: number;
enabled: boolean;
}>;
}
function getFacets<T extends IItem, K extends FacetableFields<T>>(
list: DcuplList,
attribute: K
): FacetResult<T, K> {
const facetValues = list.catalog.fn.facets({
attribute: attribute as string,
});
return {
attribute,
values: facetValues as FacetResult<T, K>['values'],
};
}
// Usage
const categoryFacets = getFacets<Product, 'category'>(productList, 'category');
const brandFacets = getFacets<Product, 'brand'>(productList, 'brand');
const priceFacets = getFacets<Product, 'price'>(productList, 'price');Type-Safe Aggregations
type NumericFields<T> = {
[K in keyof T]: T[K] extends number ? K : never;
}[keyof T];
type AggregationType = 'sum' | 'avg' | 'min' | 'max' | 'count';
interface AggregationResult<T extends IItem, K extends keyof T> {
attribute: K;
aggregation: AggregationType;
value: number;
}
function aggregate<T extends IItem, K extends NumericFields<T>>(
list: DcuplList,
attribute: K,
aggregation: AggregationType
): AggregationResult<T, K> {
const value = list.catalog.fn.aggregate({
attribute: attribute as string,
aggregation,
});
return {
attribute,
aggregation,
value: value || 0,
};
}
// Usage
const avgPrice = aggregate<Product, 'price'>(productList, 'price', 'avg');
const totalStock = aggregate<Product, 'rating'>(productList, 'rating', 'sum');
// Error: Can't aggregate non-numeric field
const invalidAgg = aggregate<Product, 'name'>(productList, 'name', 'sum'); // ✗ ErrorType-Safe Model Builder
class ModelBuilder<T extends Record<string, any>> {
private properties: PropertyDefinition[] = [];
string<K extends keyof T>(key: K, options?: { filter?: boolean; index?: boolean }): this {
this.properties.push({
key: key as string,
type: 'string',
...options,
});
return this;
}
number<K extends keyof T>(key: K, options?: { filter?: boolean; aggregate?: boolean }): this {
this.properties.push({
key: key as string,
type: 'float',
...options,
});
return this;
}
boolean<K extends keyof T>(key: K, options?: { filter?: boolean }): this {
this.properties.push({
key: key as string,
type: 'boolean',
...options,
});
return this;
}
array<K extends keyof T>(key: K, itemType: string): this {
this.properties.push({
key: key as string,
type: `Array<${itemType}>`,
});
return this;
}
build(modelKey: string): ModelDefinition {
return {
key: modelKey,
properties: this.properties,
};
}
}
// Usage
interface ProductSchema {
name: string;
price: number;
category: string;
inStock: boolean;
tags: string[];
}
const productModelDef = new ModelBuilder<ProductSchema>()
.string('name')
.number('price')
.string('category')
.boolean('inStock')
.array('tags', 'string')
.build('Product');Strict Null Checks
Handle nullable values properly:
interface ProductWithOptionals extends IItem {
name: string;
price: number;
description?: string; // Optional field
discount: number | null; // Nullable field
tags: string[];
}
function getProductDescription(product: ProductWithOptionals): string {
// Optional chaining
return product.description ?? 'No description available';
}
function getDiscountedPrice(product: ProductWithOptionals): number {
// Null check
if (product.discount === null) {
return product.price;
}
return product.price * (1 - product.discount);
}
function getFirstTag(product: ProductWithOptionals): string | undefined {
// Array access with optional chaining
return product.tags?.[0];
}Utility Types
Create reusable utility types:
// Extract IDs from items
type ItemId<T extends IItem> = T[']['value'];
// Pick specific fields
type ProductSummary = Pick<Product, 'name' | 'price' | 'category'>;
// Omit fields
type ProductWithoutMetadata = Omit<Product, ' | 'createdAt' | 'updatedAt'>;
// Partial updates
type ProductUpdate = Partial<ProductWithoutMetadata>;
// Required fields
type RequiredProduct = Required<Product>;
// Readonly
type ImmutableProduct = Readonly<Product>;
// Record type for indexed access
type ProductsById = Record<string, Product>;Enums and Literal Types
Use enums or literal types for constrained values:
// Enum
enum ProductCategory {
Electronics = 'Electronics',
Clothing = 'Clothing',
Books = 'Books',
HomeAndGarden = 'Home & Garden',
}
// Literal type
type SortOrder = 'asc' | 'desc';
type SortAttribute = 'name' | 'price' | 'rating' | 'createdAt';
interface SortOptions {
attribute: SortAttribute;
order: SortOrder;
}
function sortProducts(list: DcuplList, options: SortOptions): Product[] {
return list.catalog.query.items({
sort: {
attributes: [options.attribute],
order: [options.order],
},
}) as Product[];
}
// Usage
const sorted = sortProducts(productList, {
attribute: 'price', // ✓ Valid
order: 'asc', // ✓ Valid
});
const invalid = sortProducts(productList, {
attribute: 'invalid', // ✗ Error: Invalid attribute
order: 'ascending', // ✗ Error: Invalid order
});Type-Safe Event Handling
type DcuplEventType = 'data:updated' | 'data:added' | 'data:removed' | 'query:changed';
interface DcuplEvent<T extends DcuplEventType> {
type: T;
model?: string;
data?: any;
}
type EventHandler<T extends DcuplEventType> = (event: DcuplEvent<T>) => void;
class TypedEventEmitter {
private handlers = new Map<DcuplEventType, Set<EventHandler<any>>>();
on<T extends DcuplEventType>(eventType: T, handler: EventHandler<T>): () => void {
if (!this.handlers.has(eventType)) {
this.handlers.set(eventType, new Set());
}
this.handlers.get(eventType)!.add(handler);
// Return unsubscribe function
return () => {
this.handlers.get(eventType)?.delete(handler);
};
}
emit<T extends DcuplEventType>(event: DcuplEvent<T>): void {
this.handlers.get(event.type)?.forEach((handler) => {
handler(event);
});
}
}
// Usage
const emitter = new TypedEventEmitter();
const unsubscribe = emitter.on('data:updated', (event) => {
// event is typed as DcuplEvent<'data:updated'>
console.log('Data updated for model:', event.model);
});Testing with Types
import { describe, it, expect } from 'vitest';
import { Dcupl } from '@dcupl/core';
import type { DcuplList } from '@dcupl/common';
describe('Product Queries', () => {
let dcupl: Dcupl;
let productList: DcuplList;
beforeEach(async () => {
dcupl = new Dcupl();
dcupl.models.set(productModel);
await dcupl.init();
productList = dcupl.lists.create({ modelKey: 'Product' });
});
it('should filter products by category', () => {
productList.catalog.query.addCondition({
operator: 'eq',
attribute: 'category',
value: 'Electronics',
});
const results = productList.catalog.query.items() as Product[];
expect(results).toBeInstanceOf(Array);
expect(results.length).toBeGreaterThan(0);
expect(results.every((p) => p.category === 'Electronics')).toBe(true);
});
it('should type-check product properties', () => {
const products = productList.catalog.query.items() as Product[];
const firstProduct = products[0];
// TypeScript ensures these properties exist
expect(typeof firstProduct.name).toBe('string');
expect(typeof firstProduct.price).toBe('number');
expect(typeof firstProduct.inStock).toBe('boolean');
});
});Best Practices
1. Always Define Interfaces
// ✓ Good: Clear interface definition
interface Product extends IItem {
name: string;
price: number;
}
const products = list.catalog.query.items() as Product[];
// ✗ Bad: Using any
const products = list.catalog.query.items() as any[];2. Use Type Guards
// ✓ Good: Type guard for runtime safety
function isValidProduct(item: any): item is Product {
return (
typeof item === 'object' &&
'name' in item &&
'price' in item &&
typeof item.name === 'string' &&
typeof item.price === 'number'
);
}
const items = list.catalog.query.items();
const products = items.filter(isValidProduct); // Type: Product[]
// ✗ Bad: Unsafe type assertion
const products = items as Product[];3. Leverage Type Inference
// ✓ Good: Let TypeScript infer when possible
const products = getProducts<Product>(list);
// ✗ Bad: Redundant type annotations
const products: Product[] = getProducts<Product>(list);4. Use Generics for Reusability
// ✓ Good: Generic reusable function
function findById<T extends IItem>(list: DcuplList, id: string): T | undefined {
return list.catalog.query.first() as T | undefined;
}
// ✗ Bad: Specific implementations
function findProductById(list: DcuplList, id: string): Product | undefined {
// ...
}
function findCategoryById(list: DcuplList, id: string): Category | undefined {
// ...
}5. Document Complex Types
/**
* Represents a product in the catalog with all its attributes
* @property {string} name - The product display name
* @property {number} price - Price in USD cents (e.g., 1999 = $19.99)
* @property {boolean} inStock - Whether the product is currently available
*/
interface Product extends IItem {
name: string;
price: number;
inStock: boolean;
}What's Next?
Now that you understand TypeScript usage with dcupl, explore:
- React Integration - Use typed dcupl with React
- Vue Integration - Use typed dcupl with Vue
- Getting Started - Getting started with dcupl
- API Reference - Complete type reference
- Advanced Patterns - Build type-safe applications