---
title: Type annotations
description: Add types to exactly the SQL that deserves them. Annotations check your assumptions and never change your results.
species: concept
---

# Type annotations

Typesql is SQL with optional types. You annotate the places where you're depending
on an assumption — this column is never null, this number fits `decimal(28,2)`,
this json has a `plan` field — and the checker verifies the assumption against
your lake's actual schema, before the query runs and every time the schema
changes underneath it.

```typesql check=p1
select
  id,
  amount satisfies integer!,
  metadata::json<{plan: string}> ->> 'plan' as plan
from titan.stripe.charges
```

## The guarantee

Annotations never change what a query returns. Strip every annotation from a
typesql document and you have plain SQL that runs identically — same rows, same
values, same everything. This is a structural guarantee, not a convention: types
are checked and then erased; no execution path can even read them.

So there is no migration decision to make. Annotate one expression in one query
and stop there, or type a whole model — every point between "plain SQL" and
"fully typed" is a working program.

## `satisfies`: check an expression

Write `expr satisfies type` anywhere an expression is legal:

```typesql check=p1
select
  round(sum(amount) / 100.0, 2) satisfies decimal(38,2) as gross_volume
from titan.stripe.charges
where not _deleted
```

If a schema change ever makes that expression something other than a decimal, the
model fails its check with a diagnostic pointing at this exact span — instead of a
dashboard quietly rendering garbage. Every diagnostic carries a stable `TS` code,
the location, and a `help:` line with the fix.

`satisfies` is not a cast. `amount::string` converts a value; `amount satisfies
string` asserts a fact, and fails the check if the fact is false. Use `::` to
change data, `satisfies` to catch drift.

## Non-null: `!` and narrowing

`string!` means "a string, and never null". The checker understands how your query
logic affects nullability — this passes:

```typesql check=p1
select email satisfies string!
from titan.stripe.customers
where email is not null
```

Remove the `where` clause and it fails: `email` is nullable in the schema, and
nothing in the query has ruled the nulls out. The reverse also holds — a column
that arrives non-null becomes nullable on the outer side of a `left join`, and the
checker tracks that too. The check reflects what the query does, not just what the
schema says.

## Typed CTE headers

A CTE header can declare its columns' types — turning the boundary between query
stages into a checked contract:

```typesql check=p1
with monthly(month: date!, gross_volume: decimal(38,2)) as (
  select
    date_trunc('month', created_at)::date,
    round(sum(amount) / 100.0, 2)
  from titan.stripe.charges
  where not _deleted
  group by 1
)
select * from monthly where gross_volume > 250000
```

The body is checked against the header, and everything downstream of `monthly`
gets the declared types. When it's stripped, the header loses its types —
`with monthly(month, gross_volume) as (…)` — and runs as ordinary SQL.

## Typed json

Give a json column a shape, and accesses into it stop being guesses:

```typesql check=p1
select
  metadata::json<{plan: string, seats: integer}> as meta,
  meta ->> 'plan' as plan
from titan.stripe.charges
```

A typo like `meta ->> 'paln'` is now a check failure, not a null column
discovered three dashboards later.

## In the editor

The query editor runs the same checker as it types. Hover any expression for its
inferred type; inlay hints show the types the checker already knows, and clicking
one writes it into the query as a real `satisfies` annotation — gradual typing,
one click at a time. Quick fixes carry the same edits the diagnostics suggest.

## Where to go next

- [Writing models](/v2/docs/models/authoring) — models are checked against your
  lake on every run, and a `-- lint:` header tunes which diagnostics block.
- The type system pages cover the full grammar: scalars, `decimal(p,s)`, list and
  struct types, table types, and how system columns like `_synced_at` are typed.
