Back to Blog

TypeScript OSDK: Type-Safe Ontology SDK for Frontend Applications

The coomia-dip TypeScript OSDK provides a fully type-safe Ontology SDK for frontend and Node.js applications. Built on code generation from the Ontology Schema, it delivers compile-time type checking, IntelliSense-driven development, and seamless React/Vue integration. The SDK communicates with the platform through a gRPC-Web gateway with automatic serialization, retry logic, and optimistic updates. This article covers the complete design from TypeScript type generation, query builder patterns, React hooks integration, real-time subscriptions, to bundle optimization.

CoomiaPublished on September 29, 20258 min read
Share this articleTwitter / X

Series: S6 Platform Engineering · Article 15 | Level: Advanced | Reading Time: 18 min

TypeScript OSDK: Type-Safe Ontology SDK for Frontend Applications

#TL;DR

The coomia-dip TypeScript OSDK provides a fully type-safe Ontology SDK for frontend and Node.js applications. Built on code generation from the Ontology Schema, it delivers compile-time type checking, IntelliSense-driven development, and seamless React/Vue integration. The SDK communicates with the platform through a gRPC-Web gateway with automatic serialization, retry logic, and optimistic updates. This article covers the complete design from TypeScript type generation, query builder patterns, React hooks integration, real-time subscriptions, to bundle optimization.

#1. Why a TypeScript OSDK

#1.1 Frontend Developer Experience

Frontend developers need a first-class SDK that integrates naturally with their tooling:

  • IntelliSense: Auto-completion for Ontology object types, properties, and actions
  • Type checking: Compile-time validation of property names and types
  • Framework integration: Native hooks for React, composables for Vue
  • Bundle efficiency: Tree-shakeable, minimal bundle impact

#1.2 Comparison with Palantir OSDK

CapabilityPalantir OSDKcoomia-dip OSDK
Type generationFull TypeScriptFull TypeScript
Framework supportReactReact + Vue
CommunicationREST/JSONgRPC-Web
Real-timeWebSocketgRPC streaming
Code gen CLI@osdk/clionto-codegen
Bundle size~50KB~35KB

#2. Type Generation Architecture

#2.1 Generation Pipeline

Code
Schema Registry → onto-codegen CLI → TypeScript Source
       │                 │                    │
  Fetch schemas    Generate types      Compile & bundle
  via gRPC         via templates       via tsc + rollup

#2.2 Generated Type Examples

TypeScript
// Auto-generated: Employee.ts
import { OntologyObject, PropertyFilter, QueryBuilder } from "@coomia-dip/osdk-core";

export interface EmployeeProperties {
  readonly name: string;
  readonly department: string;
  readonly salary: number;
  readonly hireDate: Date;
  readonly email: string | null;
  readonly managerId: string | null;
}

export interface Employee extends OntologyObject<EmployeeProperties> {
  readonly __objectType: "Employee";
  readonly name: string;
  readonly department: string;
  readonly salary: number;
  readonly hireDate: Date;
  readonly email: string | null;
  readonly managerId: string | null;

  // Type-safe link traversal
  manager(): Promise<Employee | null>;
  reports(): Promise<Employee[]>;
}

// Type-safe filters
export interface EmployeeFilters {
  name: PropertyFilter<string>;
  department: PropertyFilter<string>;
  salary: PropertyFilter<number>;
  hireDate: PropertyFilter<Date>;
  email: PropertyFilter<string | null>;
}

// Type-safe query builder
export interface EmployeeQueryBuilder
  extends QueryBuilder<Employee, EmployeeFilters> {
  where(filter: (f: EmployeeFilters) => FilterExpression): this;
  select<K extends keyof EmployeeProperties>(
    ...fields: K[]
  ): QueryBuilder<Pick<Employee, K>>;
  orderBy(field: keyof EmployeeProperties, direction?: "asc" | "desc"): this;
}

#2.3 Action Type Generation

TypeScript
// Auto-generated: actions.ts
export interface CreateEmployeeParams {
  name: string;
  department: string;
  salary: number;
  hireDate: Date;
  email?: string;
  managerId?: string;
}

export interface PromoteEmployeeParams {
  employeeId: string;
  newTitle: string;
  salaryIncrease: number;
  effectiveDate: Date;
}

export interface EmployeeActions {
  createEmployee(params: CreateEmployeeParams): Promise<ActionResult<Employee>>;
  promoteEmployee(params: PromoteEmployeeParams): Promise<ActionResult<Employee>>;
}

#3. Core SDK Implementation

#3.1 Client Initialization

TypeScript
import { OntoPlatform } from "@coomia-dip/osdk";

// Type-safe client with full IntelliSense
const client = OntoPlatform.init({
  baseUrl: "https://platform.example.com",
  auth: {
    type: "bearer",
    token: "your-token-here",
  },
  options: {
    retries: 3,
    timeout: 30000,
    enableCaching: true,
    cacheMaxAge: 60000,
  },
});

// Full type-safety
const employee = await client.objects.Employee.get("emp-001");
// employee.name -> string (IntelliSense)
// employee.salary -> number (IntelliSense)

#3.2 Query Builder

TypeScript
class TypeSafeQueryBuilder<T extends OntologyObject> {
  private filters: FilterExpression[] = [];
  private selectedFields: string[] = [];
  private ordering: OrderSpec[] = [];
  private pageSize: number = 100;

  where(buildFilter: (f: Filters<T>) => FilterExpression): this {
    const filter = buildFilter(this.filterProxy);
    this.filters.push(filter);
    return this;
  }

  select<K extends keyof T["properties"]>(...fields: K[]): QueryBuilder<Pick<T, K>> {
    this.selectedFields = fields as string[];
    return this as any;
  }

  orderBy(field: keyof T["properties"], direction: "asc" | "desc" = "asc"): this {
    this.ordering.push({ field: field as string, direction });
    return this;
  }

  limit(n: number): this {
    this.pageSize = n;
    return this;
  }

  async execute(): Promise<T[]> {
    const request = this.buildRequest();
    return await this.client.executeQuery(request);
  }

  async *stream(): AsyncGenerator<T> {
    const request = this.buildRequest();
    yield* this.client.streamQuery(request);
  }
}

// Usage
const engineers = await client.objects.Employee
  .where(f => f.department.eq("Engineering"))
  .where(f => f.salary.gt(80000))
  .select("name", "salary", "department")
  .orderBy("salary", "desc")
  .limit(50)
  .execute();

#3.3 gRPC-Web Transport

TypeScript
class GrpcWebTransport {
  private readonly channel: grpc.Channel;
  private readonly interceptors: Interceptor[];

  constructor(config: TransportConfig) {
    this.channel = new grpc.Channel(config.baseUrl);
    this.interceptors = [
      new AuthInterceptor(config.auth),
      new RetryInterceptor(config.retries),
      new TracingInterceptor(),
    ];
  }

  async unary<Req, Res>(
    method: MethodDescriptor<Req, Res>,
    request: Req,
  ): Promise<Res> {
    const pipeline = this.buildPipeline(this.interceptors);
    return await pipeline.execute(this.channel, method, request);
  }

  stream<Req, Res>(
    method: MethodDescriptor<Req, Res>,
    request: Req,
  ): AsyncIterable<Res> {
    return this.channel.serverStream(method, request);
  }
}

#4. React Integration

#4.1 React Hooks

TypeScript
import { useOntologyObject, useOntologyQuery, useAction } from "@coomia-dip/osdk-react";

// Single object hook
function EmployeeProfile({ id }: { id: string }) {
  const { data: employee, loading, error, refetch } = useOntologyObject(
    client.objects.Employee,
    id,
  );

  if (loading) return <Spinner />;
  if (error) return <ErrorBanner error={error} />;

  return (
    <div>
      <h1>{employee.name}</h1>
      <p>Department: {employee.department}</p>
      <p>Salary: ${employee.salary.toLocaleString()}</p>
    </div>
  );
}

// Query hook with filters
function EngineeringTeam() {
  const { data: engineers, loading, hasMore, loadMore } = useOntologyQuery(
    client.objects.Employee
      .where(f => f.department.eq("Engineering"))
      .orderBy("name", "asc"),
    { pageSize: 20 },
  );

  return (
    <div>
      {engineers.map(emp => (
        <EmployeeCard key={emp.id} employee={emp} />
      ))}
      {hasMore && <Button onClick={loadMore}>Load More</Button>}
    </div>
  );
}

// Action hook
function PromoteButton({ employeeId }: { employeeId: string }) {
  const { execute, loading, error } = useAction(
    client.actions.promoteEmployee,
  );

  const handlePromote = async () => {
    await execute({
      employeeId,
      newTitle: "Senior Engineer",
      salaryIncrease: 10000,
      effectiveDate: new Date(),
    });
  };

  return (
    <Button onClick={handlePromote} loading={loading}>
      Promote
    </Button>
  );
}

#4.2 Optimistic Updates

TypeScript
const { execute } = useAction(client.actions.updateEmployee, {
  optimistic: true,
  onOptimisticUpdate: (params, cache) => {
    // Immediately update the cache before server confirms
    cache.update("Employee", params.employeeId, {
      salary: params.newSalary,
    });
  },
  onError: (error, cache) => {
    // Rollback on error
    cache.rollback();
    toast.error("Update failed: " + error.message);
  },
});

#4.3 Real-Time Subscriptions

TypeScript
function LiveDashboard() {
  const { data: employees } = useOntologySubscription(
    client.objects.Employee
      .where(f => f.department.eq("Engineering")),
    {
      onUpdate: (updated) => {
        console.log("Employee updated:", updated.id);
      },
      onDelete: (deleted) => {
        console.log("Employee removed:", deleted.id);
      },
    },
  );

  return <EmployeeGrid employees={employees} />;
}

#5. Vue Integration

#5.1 Vue Composables

TypeScript
import { useOntologyObject, useOntologyQuery } from "@coomia-dip/osdk-vue";

// Composition API
export default defineComponent({
  setup() {
    const { data: employee, loading } = useOntologyObject(
      client.objects.Employee,
      "emp-001",
    );

    const { data: team, loadMore } = useOntologyQuery(
      client.objects.Employee
        .where(f => f.department.eq("Engineering")),
    );

    return { employee, loading, team, loadMore };
  },
});

#6. Bundle Optimization

#6.1 Tree Shaking

TypeScript
// Only import what you need - unused object types are tree-shaken
import { Employee } from "@coomia-dip/osdk/generated/Employee";
import { useOntologyObject } from "@coomia-dip/osdk-react";

// Bundle only includes Employee type, not all 100+ object types

#6.2 Code Splitting

TypeScript
// Lazy-load object types
const EmployeeModule = lazy(() => import("@coomia-dip/osdk/generated/Employee"));

// Dynamic import for large schemas
const client = OntoPlatform.init({
  ...config,
  lazyLoading: true, // Object type modules loaded on first access
});

#6.3 Bundle Size Analysis

ComponentSize (gzipped)
Core SDK~12KB
gRPC-Web transport~8KB
React hooks~5KB
Vue composables~4KB
Per object type~0.5KB
Total (10 types)~30KB

#7. Testing

TypeScript
describe("TypeScript OSDK", () => {
  it("provides type-safe object access", async () => {
    const employee = await client.objects.Employee.get("emp-001");
    expect(employee.name).toBeTypeOf("string");
    expect(employee.salary).toBeTypeOf("number");
  });

  it("supports type-safe queries", async () => {
    const results = await client.objects.Employee
      .where(f => f.salary.gt(50000))
      .select("name", "salary")
      .execute();

    expect(results.length).toBeGreaterThan(0);
    results.forEach(emp => {
      expect(emp.salary).toBeGreaterThan(50000);
    });
  });

  it("validates action parameters", async () => {
    // @ts-expect-error - Missing required field
    await expect(client.actions.createEmployee({ name: "John" }))
      .rejects.toThrow("Missing required field");
  });

  it("handles errors gracefully", async () => {
    await expect(client.objects.Employee.get("nonexistent"))
      .rejects.toThrow(ObjectNotFoundError);
  });
});

#8. Production Best Practices

#8.1 Code Generation CI/CD

YAML
# .gitlab-ci.yml
generate-osdk:
  stage: codegen
  script:
    - npx onto-codegen generate
        --schema-url $PLATFORM_URL
        --output ./src/generated
        --language typescript
    - npm run typecheck
    - npm run test
  artifacts:
    paths:
      - src/generated/

#8.2 Performance Recommendations

  • Enable response caching for read-heavy workloads
  • Use streaming queries for large result sets
  • Implement pagination with cursor-based page tokens
  • Use optimistic updates for responsive UIs
  • Lazy-load object type modules for large schemas

#8.3 Security Considerations

  • Never embed tokens in client-side code; use secure token exchange
  • Enable CSRF protection on the gRPC-Web gateway
  • Validate all user inputs before sending to the SDK
  • Use Content Security Policy headers

#9. Summary

The coomia-dip TypeScript OSDK delivers a premium developer experience for frontend applications through type-safe code generation and framework-native integration. Key design highlights:

  1. Full type safety: Compile-time checking for all Ontology operations
  2. Framework integration: Native React hooks and Vue composables
  3. gRPC-Web transport: Efficient binary protocol with streaming support
  4. Optimistic updates: Responsive UIs with automatic rollback on error
  5. Bundle optimized: Tree-shakeable with per-object-type code splitting

The next article will explore the coomia-dip async SDK design.