Back to Articles

Introducing TypeGraph: A Type-Safe Knowledge Graph for TypeScript

Welcome to our new Articles section.

announcement

You know that feeling when you need graph-like queries in your app but spinning up Neo4j feels like overkill? Or when you’re building a RAG system and realize your vector search needs actual structure to be useful?

That’s why we built TypeGraph.

What is it?

TypeGraph is an embedded knowledge graph library for TypeScript. You define your schema with Zod, query with a fluent builder, and it all compiles down to SQL against your existing Postgres or SQLite database.

No new infrastructure. No graph database to manage. Just import and go.

Killer features

  1. Ontology reasoning (subClassOf, implies, inverseOf) - most graph tools don’t have this
  2. Vector + graph hybrid - semantic search with structural context
  3. Full type inference through traversals - super great DX
  4. Embedded in existing SQL - no new infra

Show me the code

Here’s the deal. You define nodes and edges with Zod schemas:

import { defineNode, defineEdge, defineGraph, createStore } from "@nicia-ai/typegraph";
import { z } from "zod";

const Person = defineNode("Person", {
  schema: z.object({
    name: z.string(),
    email: z.string().email().optional(),
  }),
});

const Company = defineNode("Company", {
  schema: z.object({
    name: z.string(),
    industry: z.string(),
  }),
});

const worksAt = defineEdge("worksAt", {
  schema: z.object({
    role: z.string(),
    startDate: z.string().optional(),
  }),
});

Then wire them together in a graph with semantic rules:

import { disjointWith } from "@nicia-ai/typegraph";

const graph = defineGraph({
  id: "org",
  nodes: {
    Person: { type: Person },
    Company: { type: Company },
  },
  edges: {
    worksAt: { type: worksAt, from: [Person], to: [Company] },
  },
  ontology: [
    disjointWith(Person, Company), // A person can't also be a company
  ],
});

Create a store and start working with your data:

const store = createStore(graph, drizzleAdapter);

// Create some nodes
const alice = await store.nodes.Person.create({
  name: "Alice",
  email: "alice@acme.co",
});
const acme = await store.nodes.Company.create({
  name: "Acme Corp",
  industry: "Tech",
});

// Connect them (type-checked at compile time!)
await store.edges.worksAt.create(alice, acme, {
  role: "Engineer",
});

Now here’s where it gets interesting. Query with full type inference:

const results = await store
  .query()
  .from("Person", "p")
  .traverse("worksAt", "e")
  .to("Company", "c")
  .whereNode("p", (p) => p.name.contains("Alice"))
  .select((ctx) => ({
    person: ctx.p.name, // autocomplete works
    company: ctx.c.name, // all typed
    role: ctx.e.role, // edge props too
  }))
  .execute();

That ctx object? Fully typed based on your traversal. Your editor knows exactly what properties exist on p, c, and e.

Why not just use SQL?

You could. But have you tried writing a “find all people who work at companies in the same industry as people Alice knows” query in raw SQL? It’s a mess of self-joins and CTEs.

With TypeGraph:

const results = await store
  .query()
  .from("Person", "alice")
  .whereNode("alice", (p) => p.name.eq("Alice"))
  .traverse("knows")
  .to("Person", "friend")
  .traverse("worksAt")
  .to("Company", "friendCompany")
  .traverse("worksAt", "e", { direction: "in" })
  .to("Person", "colleague")
  .select((ctx) => ({
    name: ctx.colleague.name,
    company: ctx.friendCompany.name,
  }))
  .execute();

Each .traverse() is a hop in the graph. The query builder tracks your position and types through the whole chain.

The ontology stuff

This is where TypeGraph really shines. You can define semantic relationships between your types:

import { subClassOf, implies, inverseOf } from "@nicia-ai/typegraph";

const ontology = [
  // Movies and Documentaries are both Media
  subClassOf(Movie, Media),
  subClassOf(Documentary, Media),

  // If you're married to someone, you know them
  implies(marriedTo, knows),

  // parent/child are inverses
  inverseOf(parentOf, childOf),
];

Then when you query, TypeGraph can expand these relationships automatically:

// This finds Movies AND Documentaries
const results = await store
  .query()
  .from("Media", "m", { includeSubClasses: true })
  .select((ctx) => ctx.m)
  .execute();

// This traverses marriedTo, partnersWith, AND knows edges
const results = await store.query().from("Person", "p").traverse("knows", "e", { includeImplyingEdges: true }).to("Person", "other").execute();

The database does the reasoning for you.

Vector search built in

Building a RAG system? TypeGraph has native vector support:

import { embedding } from "@nicia-ai/typegraph";

const Document = defineNode("Document", {
  schema: z.object({
    content: z.string(),
    embedding: embedding(1536), // OpenAI dimensions
  }),
});

// Later, query by similarity
const similar = await store
  .query()
  .from("Document", "d")
  .whereNode("d", (d) =>
    d.embedding.similarTo(queryVector, 10, {
      metric: "cosine",
      minScore: 0.7,
    }),
  )
  .select((ctx) => ctx.d)
  .execute();

Uses pgvector on Postgres, sqlite-vec on SQLite. Same API either way.

What else?

A few more things worth mentioning:

Cursor-based pagination that’s actually O(1):

const page = await store
  .query()
  .from("Person", "p")
  .select((ctx) => ctx.p)
  .paginate({ first: 20, after: cursor });
// Returns { data, hasNextPage, nextCursor }

Streaming for large result sets:

for await (const batch of store.query()...stream({ batchSize: 100 })) {
  // Process without loading everything into memory
}

Aggregations with proper typing:

const stats = await store
  .query()
  .from("Person", "p")
  .traverse("worksAt", "e")
  .to("Company", "c")
  .groupBy("c", "name")
  .selectAggregate({
    company: field("c", "name"),
    headcount: count("p"),
  })
  .execute();

Delete behaviors that make sense:

edges: {
  worksAt: {
    type: worksAt,
    from: [Person],
    to: [Company],
    onDelete: "cascade", // or "restrict" or "disconnect"
  },
}

Schema migrations that just work:

// TypeGraph stores its schema in the database
// Diffs are computed automatically on startup
await store.migrate(); // Applies pending changes

When to use TypeGraph

TypeGraph is great for:

  • Knowledge graphs and RAG systems
  • Apps with complex relationships (permissions, social features, recommendations)
  • Teams that want graph queries without graph database ops
  • Projects already using Postgres or SQLite

It’s probably not the right choice for:

  • Billions of edges (use a graph native database)
  • Heavy graph algorithms like PageRank (use specialized tools)
  • Distributed graph processing (that’s a different beast)

Why we built it

[TODO: Fill in your motivation here]

Get started

npm install @nicia-ai/typegraph

Check out the documentation or dive into the examples.

We’d love to hear what you build with it.