TutorialTuesday, March 31, 20269 min read

Type-Safe Data Imports with TypeScript and Xlork

How to define fully typed import schemas, get end-to-end type safety from file upload to database write, and eliminate runtime surprises using TypeScript with Xlork's SDK.

Type-Safe Data Imports with TypeScript and Xlork

Runtime type errors in data import pipelines are some of the hardest bugs to track down. A user uploads a file, your code maps columns to fields, data flows into the database — and six hours later, a support ticket appears because a numeric field was silently stored as a string. TypeScript can eliminate the entire class of these errors if you wire it correctly from schema definition all the way through to database write. This tutorial shows you how.

You'll define a fully typed import schema using Xlork's TypeScript types, derive your application's domain types from that schema, and build a validated pipeline where type errors surface at compile time rather than in production. The code patterns here apply to any data domain — contacts, products, invoices, events — by swapping the field definitions.

1Why Type Safety Matters Specifically for Import Pipelines

Import pipelines are boundary code. They sit at the edge of your system, accepting data from an external source (a user's file) and handing it to internal systems (your database, your business logic). Boundary code is where unvalidated data causes the most damage, because errors here propagate silently into storage.

Without TypeScript types tying your schema definition to your processing code, you're relying on runtime checks and human memory. Change the field name in your schema? You'll find out the webhook handler still references the old name when it throws a key error in production. Add a required field to the schema? Your database insert function silently sends null until a NOT NULL constraint fails.

  • Schema drift: field keys defined in your importer config diverge from field keys used in processing code
  • Silent null coercion: optional fields that become required at the DB layer but aren't caught at the boundary
  • Incorrect type assumptions: treating a number field as a string in downstream code, causing NaN in calculations
  • Missing field handling: adding a new field to the schema without updating the insert query or transform function
  • Enum exhaustiveness: new enum values added to a field aren't handled by switch statements in business logic

2Setting Up the Typed Schema

Start by installing the Xlork Node.js SDK with its bundled TypeScript definitions:

npm install @xlork/node
# TypeScript types are included — no @types package needed

Xlork's `XlorkField` type describes a single field in your import schema. Define your fields as a typed array, and immediately you get autocomplete on the `type`, `validators`, and `required` properties:

import type { XlorkField } from '@xlork/node';

export const productFields = [
  {
    key: 'sku',
    label: 'SKU',
    type: 'string',
    required: true,
    validators: [{ validate: 'unique' }],
  },
  {
    key: 'name',
    label: 'Product Name',
    type: 'string',
    required: true,
    validators: [{ validate: 'length_max', max: 255 }],
  },
  {
    key: 'price',
    label: 'Price (USD)',
    type: 'number',
    required: true,
    validators: [{ validate: 'number_min', min: 0 }],
  },
  {
    key: 'category',
    label: 'Category',
    type: 'string',
    required: false,
  },
  {
    key: 'in_stock',
    label: 'In Stock',
    type: 'boolean',
    required: false,
  },
  {
    key: 'description',
    label: 'Description',
    type: 'string',
    required: false,
    validators: [{ validate: 'length_max', max: 2000 }],
  },
] satisfies XlorkField[];

💡 Pro tip

The `satisfies` keyword (TypeScript 4.9+) validates the array elements against XlorkField[] while preserving the literal types of individual fields. This is important: it lets you derive a precise union type from the `key` values in the next step, rather than widening to `string`.

3Deriving Domain Types from the Schema

Now that your schema is typed with precise literal keys, you can derive your application's domain type from it. This means your `Product` type is not written independently — it's generated from the same source of truth as the importer schema. When you add a field to the schema, the type updates automatically.

// Extract the field keys as a union type
type ProductFieldKey = typeof productFields[number]['key'];
// => 'sku' | 'name' | 'price' | 'category' | 'in_stock' | 'description'

// Map keys to their runtime types
type XlorkTypeMap = {
  string: string;
  number: number;
  boolean: boolean;
  date: Date;
};

// Helper: look up a field's type by key
type FieldType<K extends ProductFieldKey> = XlorkTypeMap[
  Extract<typeof productFields[number], { key: K }>['type']
];

// Build the full product type, marking optional fields nullable
export type ImportedProduct = {
  [K in ProductFieldKey]: Extract<
    typeof productFields[number],
    { key: K }
  >['required'] extends true
    ? FieldType<K>
    : FieldType<K> | null;
};
// Resolves to:
// {
//   sku: string;
//   name: string;
//   price: number;
//   category: string | null;
//   in_stock: boolean | null;
//   description: string | null;
// }

This is advanced TypeScript, but the payoff is real. `ImportedProduct` is derived entirely from `productFields`. You never manually declare a `Product` interface that can drift from your schema definition.

4Typing the Webhook Payload

When Xlork fires a webhook, the `rows` array in the payload contains objects whose keys match your field definitions. Type the webhook handler to consume a typed rows array:

import { XlorkClient } from '@xlork/node';
import type { XlorkWebhookPayload } from '@xlork/node';
import type { Request, Response } from 'express';
import { ImportedProduct } from './schema';

const xlork = new XlorkClient({ apiKey: process.env.XLORK_API_KEY! });

interface ProductImportPayload extends XlorkWebhookPayload {
  rows: ImportedProduct[];
}

export async function handleProductImport(
  req: Request,
  res: Response
): Promise<void> {
  const signature = req.headers['x-xlork-signature'] as string;
  const isValid = xlork.webhooks.verify({
    payload: JSON.stringify(req.body),
    signature,
    secret: process.env.XLORK_WEBHOOK_SECRET!,
  });

  if (!isValid) {
    res.status(401).json({ error: 'Invalid signature' });
    return;
  }

  res.status(200).json({ received: true });

  const { rows } = req.body as ProductImportPayload;
  // rows is ImportedProduct[] — fully typed
  await insertProducts(rows);
}

5Type-Safe Database Inserts

With `ImportedProduct` in scope, your database insert function can enforce the same type contract. If you're using an ORM like Prisma, your Prisma-generated types and `ImportedProduct` should align — and TypeScript will tell you when they don't:

import { PrismaClient } from '@prisma/client';
import type { ImportedProduct } from './schema';

const prisma = new PrismaClient();

export async function insertProducts(
  products: ImportedProduct[]
): Promise<{ inserted: number; updated: number; failed: number }> {
  let inserted = 0;
  let updated = 0;
  let failed = 0;

  for (const product of products) {
    try {
      const existing = await prisma.product.findUnique({
        where: { sku: product.sku },
      });

      if (existing) {
        await prisma.product.update({
          where: { sku: product.sku },
          data: {
            name: product.name,
            price: product.price,
            category: product.category ?? existing.category,
            inStock: product.in_stock ?? existing.inStock,
            description: product.description ?? existing.description,
          },
        });
        updated++;
      } else {
        await prisma.product.create({
          data: {
            sku: product.sku,
            name: product.name,
            price: product.price,
            category: product.category,
            inStock: product.in_stock ?? false,
            description: product.description,
          },
        });
        inserted++;
      }
    } catch {
      failed++;
    }
  }

  return { inserted, updated, failed };
}

TypeScript enforces that every field access on `product` is valid. If you add a `weight` field to `productFields` but forget to include it in the Prisma `create` call, the compiler catches it. The error surfaces during `tsc` — not in a production insert.

6Typing the React Component

On the client side, Xlork's React SDK exports a typed `onComplete` callback. Wire it to your schema type for end-to-end type safety:

import { XlorkImporter } from '@xlork/react';
import type { XlorkImportResult } from '@xlork/react';
import type { ImportedProduct } from './schema';

interface ProductImportResult extends XlorkImportResult {
  rows: ImportedProduct[];
}

export function ProductImportButton() {
  const [open, setOpen] = useState(false);

  function handleComplete(result: ProductImportResult) {
    const { rows } = result;
    // rows: ImportedProduct[] — autocomplete works, type errors caught here
    console.log(`Imported ${rows.length} products`);
    console.log(`First product SKU: ${rows[0].sku}`);
    setOpen(false);
  }

  return (
    <>
      <button onClick={() => setOpen(true)}>Import Products</button>
      <XlorkImporter
        importerId={process.env.NEXT_PUBLIC_XLORK_IMPORTER_ID!}
        open={open}
        onComplete={handleComplete}
        onClose={() => setOpen(false)}
      />
    </>
  );
}

7Runtime Validation as a Safety Net

TypeScript types are erased at runtime. A malformed webhook payload or an SDK version mismatch can still deliver unexpected shapes. For production-critical import pipelines, add a runtime validation layer using Zod:

import { z } from 'zod';

export const ImportedProductSchema = z.object({
  sku: z.string().min(1),
  name: z.string().min(1).max(255),
  price: z.number().min(0),
  category: z.string().nullable(),
  in_stock: z.boolean().nullable(),
  description: z.string().max(2000).nullable(),
});

export type ImportedProduct = z.infer<typeof ImportedProductSchema>;
// Same type, now with runtime parsing

// In your webhook handler:
const parseResult = z.array(ImportedProductSchema).safeParse(req.body.rows);
if (!parseResult.success) {
  console.error('Invalid row shape:', parseResult.error.format());
  // log and handle gracefully rather than crashing
}
const rows = parseResult.data ?? [];
await insertProducts(rows);

💡 Pro tip

Using Zod as your source of truth means you get both the TypeScript type (via z.infer) and runtime validation from the same schema definition — eliminating the duplication between a type declaration and a Zod schema.

8Keeping Schema, Types, and Database in Sync

The biggest ongoing maintenance risk in a typed import pipeline is the three-way sync between your Xlork field definitions, your TypeScript types, and your database schema. You want changes to any one to immediately surface mismatches in the others.

  • Keep `productFields` (Xlork schema) and the Zod schema in the same file. Changes to one should immediately prompt updating the other.
  • Run `prisma generate` as part of your CI pipeline so Prisma types stay current with migrations.
  • Add a type-level test that assigns a mock `ImportedProduct` to `Prisma.ProductCreateInput` — if the shapes diverge, this assignment fails at compile time.
  • Use `exactOptionalPropertyTypes: true` in tsconfig to catch optional vs nullable field mismatches between your schema and the database model.

The goal is a single definition of truth: your Xlork field array. Everything downstream — the TypeScript type, the Zod validator, the Prisma insert — should be derivable from or checkable against that definition.

9Summary

Type-safe data imports are achievable without excessive boilerplate. The pattern is: define your schema with `satisfies XlorkField[]`, derive your domain type from it, type your webhook payload with that domain type, and add a Zod layer for runtime validation. Changes to the schema propagate as compile-time errors rather than production bugs.

💡 Pro tip

The Xlork Node.js SDK ships with complete TypeScript definitions. See the full type reference at xlork.com/docs/typescript. Start with the free tier — it includes all TypeScript-compatible SDK features with no usage restrictions on schema complexity.

#csv-import#data-engineering#best-practices#tutorial

Ready to simplify data imports?

Drop a production-ready CSV importer into your app. Free tier included, no credit card required.