0Pricing
TypeScript Academy · Lesson

Relations and Joins with Types

Model relations so joined results are correctly typed.

Relations and Joins with Types is a free TypeScript Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Typed Relations and Nested Results

Real schemas have relations: a user has many posts, a post belongs to a user. A type-safe ORM lets you declare these relations and then returns correctly nested, typed results from relational queries.

Declaring Relations

Alongside tables you define a relations object describing how they connect. This metadata powers both the SQL generation and the result type.

import { relations } from "drizzle-orm";

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

A Relational Query

The query API lets you request related rows with with. The result nests the related data under the relation name.

const result = await db.query.users.findMany({
  with: { posts: true },
});
// result: (User & { posts: Post[] })[]

one vs many Shapes the Type

A many relation yields an array; a one relation yields a single object (or null). The nested type matches the cardinality you declared.

const r = await db.query.posts.findMany({
  with: { author: true },
});
// r: (Post & { author: User })[]   // author is a single object

Selecting Columns Inside Relations

You can narrow both the parent and the nested rows. The result type narrows on every level independently.

const r = await db.query.users.findMany({
  columns: { id: true, name: true },
  with: {
    posts: { columns: { title: true } },
  },
});
// r: { id: number; name: string; posts: { title: string }[] }[]

Manual Joins With Typed Columns

Lower-level joins also stay typed. The join predicate uses typed column references and the select decides the flat result shape.

const rows = await db
  .select({ author: users.name, title: posts.title })
  .from(posts)
  .innerJoin(users, eq(users.id, posts.authorId));
// rows: { author: string; title: string }[]

left join Introduces Nullability

A LEFT JOIN can produce missing right-side rows, so the joined columns become nullable in the result type. The ORM reflects this automatically.

const rows = await db
  .select({ name: users.name, title: posts.title })
  .from(users)
  .leftJoin(posts, eq(posts.authorId, users.id));
// rows: { name: string; title: string | null }[]

Nested vs Flat Results

Relational queries (findMany + with) give nested objects; manual joins give flat rows. Choose nested when you want object graphs, flat when you want tabular projections.

How Nesting Is Typed

The relations metadata is read at the type level. The query type intersects the base row with an object whose keys are the requested relations, each mapped to an array or single type per its cardinality.

type WithPosts = User & { posts: Post[] };
// "many" -> Post[], "one" -> User (or User | null for optional)

Deeply Nested Relations

Relations compose. You can request a relation of a relation, and the type nests just as deeply, all inferred.

await db.query.users.findMany({
  with: { posts: { with: { comments: true } } },
});
// User & { posts: (Post & { comments: Comment[] })[] }

Why This Matters

Hand-typing nested join results is error-prone and drifts from the schema. Deriving them from declared relations keeps the object graph type and the SQL in lockstep, so a relation rename is a compile error, not a production surprise.

Quick Check

Test your grasp of typed relations and joins.

Recap

You declared relations, then let the ORM produce nested typed results: many gives arrays, one gives single objects, partial selects narrow each level, LEFT JOINs introduce nullability, and deeply nested relations compose. The relation metadata drives both SQL and result types, keeping them in sync.

Frequently asked questions

Is the “Relations and Joins with Types” lesson free?

Yes — the full text of “Relations and Joins with Types” is free to read here on the web, and the TypeScript Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO.

What will I learn in “Relations and Joins with Types”?

Model relations so joined results are correctly typed. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start TypeScript Academy?

No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Relations and Joins with Types” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this TypeScript Academy lesson?

Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Schema-to-Type Mapping
  2. Type-Safe Query Construction
  3. Inferring Query Result Shapes
  4. Relations and Joins with Types
← Back to TypeScript Academy