TypeScript Advanced Guide
Master TypeScript with jsonpathx. Learn about generic type parameters, type inference, custom type guards, and advanced patterns for building type-safe applications.
Table of Contents
- Overview
- Generic Type Parameters
- Type Inference
- QueryBuilder Type Safety
- Custom Type Guards
- Type Narrowing
- Utility Types
- Strict Mode Compatibility
- Advanced Patterns
- Type-Safe APIs
- Troubleshooting
Overview
jsonpathx is built with TypeScript-first design, providing comprehensive type definitions and support for generic type parameters throughout the API.
Type Safety Benefits
typescript
import { JSONPath } from '@jsonpathx/jsonpathx';
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
// Type-safe query
const users = await JSONPath.query<User>('$.users[*]', data);
// users: User[]
// TypeScript knows the structure
users.forEach(user => {
console.log(user.name); // ✓ Type-safe
// console.log(user.invalid); // ✗ Compile error
});Generic Type Parameters
JSONPath.query<T>
Specify the type of values you expect to retrieve:
typescript
interface Product {
id: string;
name: string;
price: number;
}
// Query returns Product[]
const products = await JSONPath.query<Product>(
'$.products[*]',
data
);
// Query returns string[]
const names = await JSONPath.query<string>(
'$.products[*].name',
data
);
// Query returns number[]
const prices = await JSONPath.query<number>(
'$.products[*].price',
data
);QueryBuilder<T>
The QueryBuilder uses generics for type inference through the chain:
typescript
interface Book {
title: string;
author: string;
price: number;
isbn: string;
}
const books = await JSONPath.create<Book>(data)
.query('$.books[*]')
.filter(book => book.price < 20) // book: Book
.sort((a, b) => a.price - b.price) // a, b: Book
.map(book => book.title) // Returns string[]
.execute();
// books: string[]Multiple Generic Parameters
Functions can use multiple type parameters:
typescript
async function transformQuery<TInput, TOutput>(
path: string,
data: unknown,
transform: (input: TInput) => TOutput
): Promise<TOutput[]> {
const results = await JSONPath.query<TInput>(path, data);
return results.map(transform);
}
// Usage
interface User { name: string; age: number; }
interface UserSummary { name: string; }
const summaries = await transformQuery<User, UserSummary>(
'$.users[*]',
data,
user => ({ name: user.name })
);Type Inference
Automatic Type Inference
TypeScript can infer types from context:
typescript
interface Data {
users: Array<{ id: number; name: string }>;
}
const data: Data = { users: [{ id: 1, name: 'Alice' }] };
// Type inferred from data structure
const users = await JSONPath.query('$.users[*]', data);
// users: unknown[] (no explicit type parameter)
// Better: Explicit type for safety
const users = await JSONPath.query<Data['users'][number]>(
'$.users[*]',
data
);
// users: { id: number; name: string }[]Type Inference in Chains
Types flow through method chains:
typescript
interface Item {
value: number;
label: string;
}
const result = await JSONPath.create<Item>(data)
.query('$.items[*]')
.filter((item): item is Item => item.value > 10)
// After filter, type is Item[]
.map(item => item.label)
// After map, type is string[]
.execute();
// result: string[]Indexed Access Types
Access nested types safely:
typescript
interface Store {
books: Array<{
title: string;
author: string;
price: number;
}>;
}
// Extract book type
type Book = Store['books'][number];
const books = await JSONPath.query<Book>('$.books[*]', data);QueryBuilder Type Safety
Typed Builder Creation
typescript
interface Product {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
}
interface ProductData {
products: Product[];
}
const data: ProductData = { /* ... */ };
// Create typed builder
const builder = JSONPath.create<Product>(data);Type Preservation Through Chains
typescript
const result = await JSONPath.create<Product>(data)
.query('$.products[*]')
// Type: Product[]
.filter((p): p is Product => p.price < 100)
// Type: Product[]
.map(p => ({
id: p.id,
name: p.name,
displayPrice: `$${p.price.toFixed(2)}`
}))
// Type: { id: string; name: string; displayPrice: string }[]
.execute();
// result: { id: string; name: string; displayPrice: string }[]Constraining Generic Types
typescript
// Constrain to objects with specific properties
function queryWithId<T extends { id: string | number }>(
path: string,
data: unknown
): Promise<T[]> {
return JSONPath.query<T>(path, data);
}
interface User {
id: number;
name: string;
}
const users = await queryWithId<User>('$.users[*]', data);
// ✓ Valid: User has id property
interface Invalid {
name: string;
}
// ✗ Error: Invalid doesn't have id property
// const items = await queryWithId<Invalid>('$.items[*]', data);Custom Type Guards
Creating Type Guards
typescript
interface User {
id: number;
name: string;
email: string;
}
interface Admin extends User {
role: 'admin';
permissions: string[];
}
// Type guard function
function isAdmin(user: User): user is Admin {
return 'role' in user && user.role === 'admin';
}
// Usage
const users = await JSONPath.query<User>('$.users[*]', data);
const admins = users.filter(isAdmin);
// admins: Admin[]Type Guards with QueryBuilder
typescript
const admins = await JSONPath.create<User>(data)
.query('$.users[*]')
.filter((user): user is Admin => {
return 'role' in user && user.role === 'admin';
})
.execute();
// admins: Admin[]Generic Type Guards
typescript
function hasProperty<T, K extends string>(
obj: T,
key: K
): obj is T & Record<K, unknown> {
return obj !== null && typeof obj === 'object' && key in obj;
}
// Usage
const results = await JSONPath.query<unknown>('$.items[*]', data);
const withName = results.filter(item => hasProperty(item, 'name'));
// withName: (unknown & Record<'name', unknown>)[]Type Narrowing
Narrowing with Filters
typescript
type Item =
| { type: 'product'; name: string; price: number }
| { type: 'service'; name: string; duration: number };
const items = await JSONPath.query<Item>('$.items[*]', data);
// Narrow to products
const products = items.filter((item): item is Extract<Item, { type: 'product' }> => {
return item.type === 'product';
});
// products: { type: 'product'; name: string; price: number }[]Narrowing with Type Guards in Callbacks
typescript
interface Result {
success: boolean;
data?: string;
error?: string;
}
const results = await JSONPath.query<Result>('$.results[*]', data);
results.forEach(result => {
if (result.success && result.data) {
// TypeScript narrows: result.data is string
console.log(result.data.toUpperCase());
}
});Discriminated Unions
typescript
type Response =
| { status: 'success'; data: unknown }
| { status: 'error'; message: string }
| { status: 'pending' };
const responses = await JSONPath.query<Response>('$.responses[*]', data);
responses.forEach(response => {
switch (response.status) {
case 'success':
// response.data is available
console.log(response.data);
break;
case 'error':
// response.message is available
console.error(response.message);
break;
case 'pending':
// No additional properties
console.log('Pending...');
break;
}
});Utility Types
Extracting Types from Results
typescript
import type {
QueryResultEntry,
AllTypesResult,
ParentChainResult
} from '@jsonpathx/jsonpathx';
// Use built-in utility types
type Entry = QueryResultEntry;
type AllResults = AllTypesResult;
// Extract value types
type ValueType = QueryResultEntry['value'];
type PathType = QueryResultEntry['path'];Creating Utility Types
typescript
// Extract array element type
type ArrayElement<T> = T extends (infer U)[] ? U : never;
interface Data {
items: Array<{ id: number; name: string }>;
}
type Item = ArrayElement<Data['items']>;
// { id: number; name: string }
// Make all properties optional
type PartialQuery<T> = {
[K in keyof T]?: T[K];
};
// Deep partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};Conditional Types
typescript
// Conditional return type based on resultType
type QueryResult<T, R extends ResultType> =
R extends 'value' ? T[] :
R extends 'path' ? string[] :
R extends 'pointer' ? string[] :
R extends 'all' ? AllTypesResult :
unknown[];
function typedQuery<T, R extends ResultType = 'value'>(
path: string,
data: unknown,
resultType?: R
): Promise<QueryResult<T, R>> {
return JSONPath.query(path, data, { resultType }) as any;
}
// Usage
const values = await typedQuery<User>('$.users[*]', data);
// values: User[]
const paths = await typedQuery<User, 'path'>('$.users[*]', data, 'path');
// paths: string[]Strict Mode Compatibility
Enabling Strict Mode
json
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitAny": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}Handling Nullable Values
typescript
interface User {
id: number;
name: string;
email?: string; // Optional
}
const users = await JSONPath.query<User>('$.users[*]', data);
users.forEach(user => {
// Strict null checks
if (user.email) {
// email is string (not string | undefined)
console.log(user.email.toUpperCase());
}
});Non-Null Assertions
typescript
const user = await JSONPath.create<User>(data)
.query('$.users[0]')
.first();
// Use non-null assertion when you're certain
const name = user!.name;
// Better: Optional chaining
const name = user?.name;
// Best: Type guard
if (user) {
const name = user.name;
}Advanced Patterns
Builder Pattern with Types
typescript
class TypedQueryBuilder<T> {
private path?: string;
query(path: string): this {
this.path = path;
return this;
}
async execute(data: unknown): Promise<T[]> {
if (!this.path) throw new Error('No path set');
return JSONPath.query<T>(this.path, data);
}
}
// Usage
const builder = new TypedQueryBuilder<User>();
const users = await builder.query('$.users[*]').execute(data);Generic Repository Pattern
typescript
interface Repository<T extends { id: string | number }> {
findAll(): Promise<T[]>;
findById(id: T['id']): Promise<T | undefined>;
findWhere(predicate: (item: T) => boolean): Promise<T[]>;
}
class JSONPathRepository<T extends { id: string | number }>
implements Repository<T> {
constructor(
private data: unknown,
private basePath: string
) {}
async findAll(): Promise<T[]> {
return JSONPath.query<T>(this.basePath, this.data);
}
async findById(id: T['id']): Promise<T | undefined> {
const results = await JSONPath.query<T>(
`${this.basePath}[?(@.id === ${JSON.stringify(id)})]`,
this.data
);
return results[0];
}
async findWhere(predicate: (item: T) => boolean): Promise<T[]> {
const all = await this.findAll();
return all.filter(predicate);
}
}
// Usage
interface Product {
id: string;
name: string;
price: number;
}
const repo = new JSONPathRepository<Product>(data, '$.products[*]');
const products = await repo.findAll();
const product = await repo.findById('123');
const cheap = await repo.findWhere(p => p.price < 100);Type-Safe Mutations
typescript
import { Mutation, MutationResult } from '@jsonpathx/jsonpathx';
async function typedSet<T, K extends keyof T>(
data: { [key: string]: T[] },
arrayKey: string,
itemPath: string,
prop: K,
value: T[K]
): Promise<MutationResult> {
return Mutation.set(
data,
`$.${arrayKey}[${itemPath}].${String(prop)}`,
value
);
}
// Usage
interface User {
id: number;
name: string;
active: boolean;
}
await typedSet<User, 'name'>(
data,
'users',
'?(@.id === 1)',
'name',
'New Name'
);Type-Safe APIs
Wrapper Functions
typescript
// Create type-safe wrapper
async function queryUsers(data: unknown): Promise<User[]> {
return JSONPath.query<User>('$.users[*]', data);
}
async function queryActiveUsers(data: unknown): Promise<User[]> {
return JSONPath.query<User>(
'$.users[?(@.active === true)]',
data
);
}
async function getUserById(data: unknown, id: number): Promise<User | undefined> {
const results = await JSONPath.query<User>(
`$.users[?(@.id === ${id})]`,
data
);
return results[0];
}Type-Safe Configuration
typescript
interface QueryConfig<T> {
path: string;
transform?: (item: T) => T;
filter?: (item: T) => boolean;
}
async function configuredQuery<T>(
data: unknown,
config: QueryConfig<T>
): Promise<T[]> {
let results = await JSONPath.query<T>(config.path, data);
if (config.filter) {
results = results.filter(config.filter);
}
if (config.transform) {
results = results.map(config.transform);
}
return results;
}
// Usage
const users = await configuredQuery<User>(data, {
path: '$.users[*]',
filter: u => u.active,
transform: u => ({ ...u, name: u.name.toUpperCase() })
});Troubleshooting
Type 'unknown' Error
typescript
// ❌ Error: Property 'name' does not exist on type 'unknown'
const results = await JSONPath.query('$.items[*]', data);
results.forEach(item => console.log(item.name));
// ✅ Fix: Add type parameter
const results = await JSONPath.query<{ name: string }>('$.items[*]', data);
results.forEach(item => console.log(item.name));Type Assertion
typescript
// When you know the type but TypeScript doesn't
const results = await JSONPath.query('$.items[*]', data);
const typed = results as Item[];
// Better: Use type parameter
const results = await JSONPath.query<Item>('$.items[*]', data);Handling Union Types
typescript
type Value = string | number | boolean;
const values = await JSONPath.query<Value>('$.values[*]', data);
values.forEach(value => {
if (typeof value === 'string') {
console.log(value.toUpperCase());
} else if (typeof value === 'number') {
console.log(value.toFixed(2));
} else {
console.log(value ? 'true' : 'false');
}
});Generic Constraints Not Met
typescript
// ❌ Error: Type 'string' does not satisfy constraint
function queryWithId<T extends { id: number }>(path: string, data: unknown) {
return JSONPath.query<T>(path, data);
}
interface Item {
id: string; // Wrong type!
name: string;
}
// const items = await queryWithId<Item>('$.items[*]', data);
// ✅ Fix: Match the constraint
interface Item {
id: number; // Correct type
name: string;
}
const items = await queryWithId<Item>('$.items[*]', data);Async/Await Types
typescript
// Return type is Promise<T[]>
async function getUsers(data: unknown): Promise<User[]> {
return JSONPath.query<User>('$.users[*]', data);
}
// Unwrap promise type
type UnwrappedUsers = Awaited<ReturnType<typeof getUsers>>;
// User[]Best Practices
1. Always Use Type Parameters
typescript
// ❌ Avoid
const items = await JSONPath.query('$.items[*]', data);
// ✅ Prefer
const items = await JSONPath.query<Item>('$.items[*]', data);2. Define Interfaces Early
typescript
// Define data structures upfront
interface AppData {
users: User[];
products: Product[];
orders: Order[];
}
interface User {
id: number;
name: string;
email: string;
}
// Use throughout application
const users = await JSONPath.query<User>('$.users[*]', data);3. Use Type Guards for Runtime Safety
typescript
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value
);
}
const results = await JSONPath.query('$.items[*]', data);
const users = results.filter(isUser);4. Leverage IDE Autocomplete
typescript
// With proper types, IDE provides autocomplete
const users = await JSONPath.query<User>('$.users[*]', data);
users.forEach(user => {
user. // IDE suggests: id, name, email
});5. Document Type Parameters
typescript
/**
* Query products with type safety
* @template T - Product type with required properties
* @param path - JSONPath expression
* @param data - Source data
* @returns Array of typed products
*/
async function queryProducts<T extends Product>(
path: string,
data: unknown
): Promise<T[]> {
return JSONPath.query<T>(path, data);
}See Also
- Type Reference - Complete type definitions
- QueryBuilder API - Builder API reference
- Examples - Advanced examples
- Strict Mode Guide - Strict mode configuration