# TypeScript usage guide

This guide shows how to type your dcupl code: which types the SDK exports, which methods take your item type as a generic, what an untyped list returns, and patterns for typed queries, facets and aggregations.

## Installation

The dcupl SDK includes built-in TypeScript definitions:

```bash group="install" tab="npm"
npm install @dcupl/core @dcupl/common
```

```bash group="install" tab="yarn"
yarn add @dcupl/core @dcupl/common
```

```bash group="install" tab="pnpm"
pnpm add @dcupl/core @dcupl/common
```

`@dcupl/core` depends on exactly its own version of `@dcupl/common`. Install both in the same version, or your type imports come from a different version of `@dcupl/common` than the one the SDK uses. The type definitions use `NoInfer`, so they need TypeScript 5.4 or later.

```json filename="tsconfig.json"
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM"],
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true
  }
}
```

## Key types

`Dcupl` and `DcuplList` come from `@dcupl/core`, `DcuplAppLoader` from `@dcupl/loader`. The types come from `@dcupl/common`:

| Type                                    | What it types                                                 |
| --------------------------------------- | ------------------------------------------------------------- |
| `ModelDefinition`, `Property`           | Models and their properties                                   |
| `ListItem`                              | A generic item: `{ key: string; [attribute: string]: any }`   |
| `DcuplQuery`, `DcuplQueryGroup`         | A query condition and a group of conditions                   |
| `DcuplQueryOperator`                    | The operator names: `'eq'`, `'in'`, `'find'`, `'gt'`, …       |
| `DcuplListQueryOptions<T>`              | The options of `items()`, `first()` and `count()`             |
| `SortDirection`                         | `'ASC'` or `'DESC'`                                           |
| `DcuplFacet`                            | One facet value: `{ key, value, size, selected, enabled, … }` |
| `Aggregation`, `AggregationType`        | The result of `fn.aggregate()` and its `types`                |
| `DcuplUpdateMessage`, `DcuplUpdateType` | An event and the union of its `type`                          |
| `DcuplInitOptions`                      | The options of `new Dcupl()`                                  |

These methods take your item type as a generic parameter: `dcupl.lists.create<T>()` (it types the list's items), `items<T>()`, `execute<T>()`, `first<T>()` and `count<T>()` on `list.catalog.query`, and `one<T>()`, `many<T>()` and `execute<T>()` on `dcupl.query`. See [Types](/sdk/api/types) for the full reference.

### Untyped lists return `any`

`dcupl.lists.create()` returns a `DcuplList` whose item type defaults to `any`. Without a generic, `items()` returns `any[]` and `first()` returns `any`, so TypeScript doesn't flag a misspelled attribute. The untyped call next to the ways to type the results:

- `list.catalog.query.items()` returns `any[]`: a misspelled attribute isn't flagged.
- `list.catalog.query.items<Product>()` returns `Product[]`: a misspelled attribute is an error.
- `dcupl.lists.create<Product>(...)` returns a `DcuplList<Product>`, the same type as the annotation `const list: DcuplList<Product> = dcupl.lists.create(...)`. On that list `items()` returns `(Product & ListItem)[]` and `first()` `(Product & ListItem) | undefined`: a misspelled attribute is `any` and isn't flagged.

`items<Product>()` is the stricter one: on a `DcuplList<Product>`, the known attributes are typed, but `ListItem`'s index signature makes every other attribute `any`.

Generics and casts only tell TypeScript what shape to expect. Nothing checks the items at runtime: for that, use a type guard ([Custom Type Guards](#custom-type-guards)) or the model's quality checks.

## Basic type usage

### Importing types

```typescript
import type { ModelDefinition, Property, ListItem, DcuplQuery, DcuplQueryOperator } from '@dcupl/common';
import { Dcupl, DcuplList } from '@dcupl/core';
```

`@dcupl/core` exports the `Dcupl` and `DcuplList` classes; the types (models, queries, facets, events) come from `@dcupl/common`. A few helper types aren't exported, see [Types](/sdk/api/types).

### Type-safe model definition

```typescript
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. Every dcupl item has a string `key`. `@dcupl/common` types a generic item as `ListItem` (`{ key: string; [attribute: string]: any }`); its index signature makes every attribute `any`, so declare your own interfaces to get checked attribute names:

```typescript
// Every dcupl item has a string key
interface DcuplItem {
  key: string;
}

interface Product extends DcuplItem {
  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 DcuplItem {
  name: string;
  slug: string;
  description: string;
  parentId: string | null;
}

interface Customer extends DcuplItem {
  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

```typescript
interface Order extends DcuplItem {
  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

`items()`, `first()` and `execute()` take the item type as a generic parameter, or you pass it once to `dcupl.lists.create<Product>()` ([Untyped Lists Return `any`](#untyped-lists-return-any)):

```typescript
import { Dcupl, DcuplList } from '@dcupl/core';

const dcupl = new Dcupl();
// Setup: register the Product model and its data (dcupl.models.set / dcupl.data.set),
// then initialize — lists.create() throws for a model that isn't registered
await dcupl.init();

// Create a list
const productList: DcuplList = dcupl.lists.create({ modelKey: 'Product' });

// The generic parameter types the results (Product is the interface defined above)
const products = productList.catalog.query.items<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:

```typescript
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,
    });
  }

  // Conditions on the same attribute combine with OR,
  // so a price range needs its own 'and' group
  const priceRange: DcuplQuery[] = [];
  if (filters?.minPrice !== undefined) {
    priceRange.push({ operator: 'gte', attribute: 'price', value: filters.minPrice });
  }
  if (filters?.maxPrice !== undefined) {
    priceRange.push({ operator: 'lte', attribute: 'price', value: filters.maxPrice });
  }
  if (priceRange.length > 0) {
    list.catalog.query.addGroup({ groupKey: 'price', groupType: 'and', queries: priceRange });
  }

  if (filters?.inStock !== undefined) {
    list.catalog.query.addCondition({
      operator: 'eq',
      attribute: 'inStock',
      value: filters.inStock,
    });
  }

  // Return typed results
  return list.catalog.query.items<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

```typescript
class DcuplRepository<T extends DcuplItem> {
  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();
    // one() looks the item up by key in the list's current result;
    // it returns null when the key isn't there: ?? turns that into undefined
    return (this.list.catalog.query.one({ itemKey: id }) ?? undefined) 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[]
```

## Typing results

`execute()` is an alias of `items()`: both return the type you pass as the generic parameter:

```typescript
// Generic parameter
const products0 = productList.catalog.query.items<Product>();

// 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 DcuplItem>(list: DcuplList): T[] {
  return list.catalog.query.items<T>();
}

const products3 = getProducts<Product>(productList); // Type: Product[]
```

## Type-safe queries

### Query builder with types

```typescript
import type { DcuplQuery, DcuplQueryOperator } from '@dcupl/common';

// 'eq' | 'in' | 'find' | 'gt' | 'gte' | 'lt' | 'lte' | 'typeof' | 'isTruthy' | 'size'
type QueryOperator = DcuplQueryOperator;

interface TypedQuery<T> {
  attribute: keyof T;
  operator: QueryOperator;
  value: any;
}

class TypedQueryBuilder<T extends DcuplItem> {
  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();

    // One 'and' group: every where() must match, also several on the same attribute
    // (addCondition would OR conditions that share an attribute)
    const queries: DcuplQuery[] = this.queries.map((query) => ({
      operator: query.operator,
      attribute: query.attribute as string,
      value: query.value,
    }));
    this.list.catalog.query.addGroup({ groupKey: 'typed', groupType: 'and', queries });

    return this.list.catalog.query.items<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

```typescript
type FilterValue<T, K extends keyof T> = T[K];

interface FilterConfig<T extends DcuplItem> {
  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 DcuplItem>(list: DcuplList): FilterConfig<T> {
  // Conditions on different attributes combine with AND; a second condition
  // on the same attribute is ORed with the first
  const applyFilter = (operator: QueryOperator, attribute: string, value: any) => {
    list.catalog.query.addCondition({ operator, attribute, value });
  };

  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: '/…/' matches a substring, a bare string the whole value
filter.eq('invalid', 'value'); // ✗ Error: Property doesn't exist
filter.eq('price', 'not-a-number'); // ✗ Error: Wrong type
```

## Custom type guards

Create type guards for runtime type checking:

```typescript
function isProduct(item: ListItem): 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: ListItem): item is Category {
  return 'name' in item && 'slug' in item && typeof item.name === 'string' && typeof item.slug === 'string';
}

// Usage
const items = productList.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

```typescript
type QueryableFields<T> = {
  [K in keyof T]: T[K] extends string | number | boolean ? K : never;
}[keyof T];

type QueryableProduct = QueryableFields<Product>;
// Type: 'key' | 'name' | 'sku' | 'price' | 'category' | 'brand' | 'description' | 'inStock' | 'rating'

function queryByField<T extends DcuplItem, 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<T>();
}

// Usage
const byName = queryByField<Product, 'name'>(productList, 'name', 'Laptop');
const byPrice = queryByField<Product, 'price'>(productList, 'price', 999);
```

### Conditional types for filters

```typescript
type FilterOperatorForType<T> = T extends string
  ? 'eq' | 'in' | 'find' | 'typeof' | 'isTruthy' | 'size'
  : T extends number
    ? 'eq' | 'in' | 'gt' | 'gte' | 'lt' | 'lte' | 'typeof'
    : T extends boolean
      ? 'eq' | 'isTruthy'
      : 'eq' | 'in' | 'typeof' | 'isTruthy';

interface TypedFilter<T extends DcuplItem, K extends keyof T> {
  attribute: K;
  operator: FilterOperatorForType<T[K]>;
  value: T[K] | T[K][];
}

function applyTypedFilter<T extends DcuplItem, K extends keyof T>(list: DcuplList, filter: TypedFilter<T, K>): T[] {
  // Cast needed: the generic attribute/operator pair isn't narrowed to one DcuplQuery variant
  list.catalog.query.addCondition({
    operator: filter.operator,
    attribute: filter.attribute as string,
    value: filter.value,
  } as DcuplQuery);
  return list.catalog.query.items<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

```typescript
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];
    size: number; // number of items with this value
    enabled: boolean;
  }>;
}

function getFacets<T extends DcuplItem, K extends FacetableFields<T>>(
  list: DcuplList,
  attribute: K,
): FacetResult<T, K> {
  // DcuplFacet[]: { key, value, size, selected, enabled, ... }
  const facetValues = list.catalog.fn.facets({
    attribute: attribute as string,
  });

  return {
    attribute,
    values: facetValues.map((facet) => ({
      value: facet.value as T[K],
      size: facet.size,
      enabled: facet.enabled,
    })),
  };
}

// 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

```typescript
type NumericFields<T> = {
  [K in keyof T]: T[K] extends number ? K : never;
}[keyof T];

type AggregationType = 'sum' | 'avg' | 'min' | 'max' | 'count'; // a subset of AggregationType from @dcupl/common

interface AggregationResult<T extends DcuplItem, K extends keyof T> {
  attribute: K;
  aggregation: AggregationType;
  value: number;
}

function aggregate<T extends DcuplItem, K extends NumericFields<T>>(
  list: DcuplList,
  attribute: K,
  aggregation: AggregationType,
): AggregationResult<T, K> {
  // Returns { attribute, types, avg?, sum?, ... } with one field per requested type
  const result = list.catalog.fn.aggregate({
    attribute: attribute as string,
    types: [aggregation],
  });

  return {
    attribute,
    aggregation,
    value: result[aggregation] ?? 0,
  };
}

// Usage
const avgPrice = aggregate<Product, 'price'>(productList, 'price', 'avg');
const ratingSum = aggregate<Product, 'rating'>(productList, 'rating', 'sum');

// Error: Can't aggregate non-numeric field
const invalidAgg = aggregate<Product, 'name'>(productList, 'name', 'sum'); // ✗ Error
```

## Strict null checks

Handle nullable values properly. An attribute the source row has no value for is absent from the item (`undefined`); an explicit `null` in the source stays `null`:

```typescript
interface ProductWithOptionals extends DcuplItem {
  name: string;
  price: number;
  description?: string; // Optional field
  discount?: number | null; // Missing (undefined) or explicitly null
  tags?: string[];
}

function getProductDescription(product: ProductWithOptionals): string {
  // Nullish coalescing
  return product.description ?? 'No description available';
}

function getDiscountedPrice(product: ProductWithOptionals): number {
  // == null catches both null and a missing value
  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];
}
```

## Typed sorting

Sort orders are uppercase; `SortDirection` from `@dcupl/common` types them:

```typescript
import type { SortDirection } from '@dcupl/common';

type SortAttribute = 'name' | 'price' | 'rating' | 'createdAt';

interface SortOptions {
  attribute: SortAttribute;
  order: SortDirection; // 'ASC' | 'DESC'
}

function sortProducts(list: DcuplList, options: SortOptions): Product[] {
  return list.catalog.query.items<Product>({
    sort: {
      attributes: [options.attribute],
      order: [options.order],
    },
  });
}

// 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

`dcupl.on()`, `list.on()` and `list.onResultChange()` pass a `DcuplUpdateMessage` and return an unsubscribe function. `message.type` is the `DcuplUpdateType` union, so TypeScript rejects event names that don't exist:

```typescript
import type { DcuplUpdateMessage } from '@dcupl/common';

// Fires whenever the list's result changes (query change or dcupl.update())
const unsubscribe = productList.onResultChange((message: DcuplUpdateMessage) => {
  console.log(message.type, message.listKey); // 'result_updated', '<list key>'
  renderProducts(productList.catalog.query.items<Product>());
});

// Global events
dcupl.on((message) => {
  if (message.type === 'dcupl_updated_manually') {
    console.log('dcupl.update() finished');
  }
  if (message.type === 'data_updated') {
    // ✗ Error: 'data_updated' is not a DcuplUpdateType
  }
});

// Later
unsubscribe();
```

## Testing with types

```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { Dcupl, DcuplList } from '@dcupl/core';

describe('Product Queries', () => {
  let dcupl: Dcupl;
  let productList: DcuplList;

  beforeEach(async () => {
    dcupl = new Dcupl();
    dcupl.models.set(productModel);
    dcupl.data.set(
      [
        { key: 'p1', name: 'Laptop', price: 999, category: 'Electronics', inStock: true, tags: ['work'] },
        { key: 'p2', name: 'Desk', price: 299, category: 'Furniture', inStock: false, tags: [] },
      ],
      { model: 'Product' },
    );
    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<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<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

```typescript
// ✓ Good: Clear interface definition, passed as the generic
interface Product extends DcuplItem {
  name: string;
  price: number;
}

const products = productList.catalog.query.items<Product>();

// ✗ Bad: Untyped results are any[]
const untyped = productList.catalog.query.items();
```

The generic, like a cast, is a promise about the shape that nothing checks at runtime.

### 2. Use type guards

```typescript
// ✓ 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 = productList.catalog.query.items();
const products = items.filter(isValidProduct); // Type: Product[], checked at runtime

// ✗ Bad: Unchecked type assertion
const unchecked = items as Product[];
```

### 3. Leverage type inference

```typescript
// ✓ Good: Let TypeScript infer when possible
const products = getProducts<Product>(productList);

// ✗ Bad: Redundant type annotations
const annotated: Product[] = getProducts<Product>(productList);
```

### 4. Use generics for reusability

```typescript
// ✓ Good: Generic reusable function
function findById<T extends DcuplItem>(list: DcuplList, id: string): T | undefined {
  // one() returns null for a key that isn't in the list's result: ?? turns that into undefined
  return (list.catalog.query.one({ itemKey: id }) ?? undefined) as T | undefined;
}

// ✗ Bad: Specific implementations
function findProductById(list: DcuplList, id: string): Product | undefined {
  // ...
}
function findCategoryById(list: DcuplList, id: string): Category | undefined {
  // ...
}
```

## Next steps

- [React Integration](/learn/tutorials/react-integration) — Use typed dcupl with React
- [Vue Integration](/learn/tutorials/vue-integration) — Use typed dcupl with Vue
- [Getting started](/learn/getting-started) — Getting started with dcupl
- [API reference](/sdk/api/types) — Complete type reference
- [Faceted search](/sdk/advanced/faceted-search) — Facets, counts and filters for search UIs
