Type-safe Notion API library with compile-time validation and runtime type inference.
- Type Safety First: End-to-end type safety from schema definition to API response
- Complex Properties: Full type inference for Rollups and Formulas using helper functions
- Zero Dependencies: Lightweight (7.75KB gzipped) and ideal for edge environments
- Runtime Validation: Protects your application from silent schema changes in Notion
npm install typed-notion-core-tsThe core philosophy is "Schema First". Define your schema, and get types for free.
import { createTypedSchema, NotionClient, rollup, formula } from 'typed-notion-core-ts';
// 1. Define your Notion database schema
const projectSchema = createTypedSchema({
title: { type: 'title' },
status: {
type: 'select',
options: ['Planning', 'In Progress', 'Done'],
},
// ✨ Automatic type inference for complex properties
taskCount: rollup('Tasks', 'Name', 'count'), // -> number | null
isOverdue: formula('boolean', 'prop("Due") < now()'), // -> boolean | null
});
// 2. Initialize the client
const client = new NotionClient({ auth: process.env.NOTION_TOKEN });
// 3. Type-safe database operations
const newProject = await client.create(projectSchema, {
title: 'Website Redesign',
status: 'Planning', // ✅ Auto-completed & Validated
// taskCount and isOverdue are read-only, excluded from creation
});
// 4. Type-safe response
const project = await client.getPage(projectSchema, newProject.id);
if (project.isOverdue) {
console.log(`Warning: ${project.title} has ${project.taskCount} tasks!`);
}Define your database structure once, use it everywhere with full type safety:
import { createTypedSchema, formula, rollup } from 'typed-notion-core-ts';
const schema = createTypedSchema({
// Basic properties
title: { type: 'title' },
completed: { type: 'checkbox' },
priority: {
type: 'select',
options: ['Low', 'Medium', 'High', 'Urgent'],
},
tags: {
type: 'multi_select',
options: ['Frontend', 'Backend', 'Design', 'Bug'],
},
assignee: { type: 'people' },
dueDate: { type: 'date' },
// Complex properties with type inference
totalBudget: rollup('Expenses', 'Amount', 'sum'), // -> number | null
lastUpdate: rollup('Activity', 'Date', 'latest'), // -> Date | null
progress: formula('number', 'prop("Completed") / prop("Total") * 100'),
statusLabel: formula('string', 'concat("Task: ", prop("Title"))'),
});Export TypeScript types directly from your runtime schema:
import { InferSchemaProperties } from 'typed-notion-core-ts';
// Automatically inferred from schema
type Project = InferSchemaProperties<typeof projectSchema>;
function processProject(project: Project) {
// TypeScript knows exact types
if (project.status === 'Done') {
// ✅ 'Planning' | 'In Progress' | 'Done'
console.log(project.taskCount); // ✅ number | null
console.log(project.title); // ✅ string
}
}const activeProjects = await client.query(projectSchema, {
filter: {
and: [
{ property: 'status', select: { equals: 'In Progress' } },
{ property: 'taskCount', number: { greater_than: 0 } },
],
},
sorts: [{ property: 'dueDate', direction: 'ascending' }],
});
// Type-safe iteration
activeProjects.results.forEach(project => {
// project is fully typed based on schema
console.log(`${project.title}: ${project.taskCount} tasks`);
});// Create - only writable properties allowed
const task = await client.create(taskSchema, {
title: 'Implement authentication',
priority: 'High',
tags: ['Backend', 'Security'],
assignee: [{ id: 'user-id' }],
});
// Update - partial updates supported
await client.update(task.id, taskSchema, {
completed: true,
tags: ['Backend', 'Security', 'Done'],
});let allProjects = [];
let hasMore = true;
let cursor = undefined;
while (hasMore) {
const response = await client.query(projectSchema, {
page_size: 100,
start_cursor: cursor,
});
allProjects.push(...response.results);
hasMore = response.has_more;
cursor = response.next_cursor;
}Helper functions provide ergonomic API for defining complex properties:
import { rollup, formula, union } from 'typed-notion-core-ts';
const advancedSchema = createTypedSchema({
// Rollup with automatic type inference
totalSpent: rollup('Transactions', 'Amount', 'sum'),
earliestTask: rollup('Tasks', 'Created', 'earliest'),
// Formula with explicit type hints
isUrgent: formula('boolean'),
displayName: formula('string'),
score: formula('number'),
// Union types for conditional formulas
dynamicValue: formula(union('string', 'number'), 'if(prop("IsText"), "Text Value", 42)'), // -> string | number | null
});Validate that your local schema matches the actual Notion database:
const validation = await client.validateSchema(projectSchema);
if (!validation.isValid) {
console.error('Schema mismatch detected:');
validation.errors.forEach(error => {
console.error(`- ${error.property}: ${error.message}`);
});
}Built-in performance tracking for optimization:
const metrics = client.getPerformanceMetrics();
console.log(`Average response time: ${metrics.averageResponseTime}ms`);
console.log(`Cache hit rate: ${metrics.cacheHitRate}%`);
// Clear cache when needed
client.clearCaches();import { DatabaseConfigManager } from 'typed-notion-core-ts';
const config = new DatabaseConfigManager();
// Set database IDs from environment
config.setDatabaseId('projects', process.env.PROJECTS_DB_ID);
config.setDatabaseId('tasks', process.env.TASKS_DB_ID);
// Auto-resolve database IDs
const projects = await client.query(projectSchema); // Uses configured IDType inference covers 100% of Notion property types. There is no implicit any.
| Property Type | TypeScript Type | Validation / Inference |
|---|---|---|
title |
string |
Required, non-empty |
rich_text |
string |
Text content (plain text) |
number |
number |
Numeric values |
select |
string (Union) |
Matches defined options exactly |
multi_select |
string[] (Union) |
Array of defined options |
date |
Date | string |
ISO date format support |
checkbox |
boolean |
True / False |
people |
NotionUser[] |
Notion user objects |
files |
File[] |
File objects |
url |
string |
Valid URL format |
email |
string |
Valid email format |
phone_number |
string |
Phone number format |
relation |
string[] |
Array of Page IDs |
formula |
T | null |
Inferred from Developer Hint (String, Number, Boolean, Date, Union) |
rollup |
T | null |
Auto-inferred from Source (Matches source property type) |
Note on Nullability: Unlike standard Notion API responses, this library treats
rollupandformularesults as nullable (| null) by default. This forces you to handle cases where calculation fails or data is missing, preventing runtime crashes.
createTypedSchema(definition)- Create a typed schema from property definitionsrollup(relation, property, function)- Define a rollup property with type inferenceformula(returnType, expression?)- Define a formula with explicit type hintunion(...types)- Create union type for complex formulas
new NotionClient(config)- Initialize Notion client with authenticationclient.query(schema, options?)- Query database with filters and sortingclient.create(schema, data)- Create new database entryclient.update(id, schema, data)- Update existing entryclient.getPage(schema, id)- Retrieve single page by IDclient.validateSchema(schema)- Validate schema against database
InferSchemaProperties<T>- Extract TypeScript type from schemaPropertyType- Union of all supported Notion property typesSchemaDefinition- Type for schema configuration object
We welcome contributions! Please see CONTRIBUTING.md for development setup, testing guidelines, and submission process.
MIT © Satoyoshi