Type Safety
AuthzKit provides end-to-end type safety throughout your authorization flow, leveraging TypeScript's powerful type system to catch errors at compile time and provide excellent developer experience.
Why Type Safety Matters
Type safety in authorization prevents common bugs and security issues:
// Without type safety - prone to errors
policy.check('viewPost', {
subject: user,
// Missing required 'resource' field - runtime error
})
// With AuthzKit type safety - compile-time error
policy.check('viewPost', {
subject: user,
resource: post // TypeScript enforces this is required
})
Type-Safe Action Definitions
Actions define the exact shape of authorization requests:
import { action, defineActions } from '@authzkit/core'
type User = {
id: string
role: 'admin' | 'user' | 'moderator'
tenantId: string
}
type Post = {
id: string
title: string
content: string
authorId: string
tenantId: string
published: boolean
}
export const actions = defineActions({
viewPost: action<{
subject: User;
resource: Post
}>(),
editPost: action<{
subject: User;
resource: Post;
changes: Partial<Pick<Post, 'title' | 'content'>> // Only these fields editable
}>(),
deletePost: action<{
subject: User;
resource: Post
}>()
})
export type Actions = typeof actions
Type-Safe Policy Rules
Policy rules are fully typed based on your action definitions:
const policy = definePolicy<Actions>({
byAction: {
editPost: [
{
id: 'user-edit-own-post',
effect: 'allow',
when: ({ subject, resource, changes }) => {
// TypeScript knows the exact types of all parameters:
// subject: User
// resource: Post
// changes: Partial<Pick<Post, 'title' | 'content'>>
return subject.id === resource.authorId
},
readMask: {
id: true,
title: true,
content: true,
// TypeScript error if field doesn't exist on Post:
// invalidField: true, // ❌ Compile error
} satisfies FieldMask<Post>,
reason: 'post-owner'
}
]
}
})
Compile-Time Validation
Required Properties
TypeScript enforces that all required properties are provided:
// ✅ Valid - all required properties present
const canView = policy.check('viewPost', {
subject: user,
resource: post
})
// ❌ TypeScript error - missing 'resource'
const canView = policy.check('viewPost', {
subject: user
// Property 'resource' is missing
})
// ❌ TypeScript error - wrong property type
const canEdit = policy.check('editPost', {
subject: user,
resource: post,
changes: "invalid" // Expected object, got string
})
Action Name Validation
Action names are validated at compile time:
// ✅ Valid action name
policy.check('viewPost', { /* ... */ })
// ❌ TypeScript error - action doesn't exist
policy.check('invalidAction', { /* ... */ })
// ^^^^^^^^^^^^^^^^ Argument of type '"invalidAction"' is not assignable
Field Mask Type Safety
Field masks are type-checked against your resource types:
{
id: 'user-view-post',
action: 'viewPost',
effect: 'allow',
when: ({ subject }) => subject.role === 'user',
readMask: {
id: true,
title: true,
content: true,
published: true,
// TypeScript error - field doesn't exist on Post:
// invalidField: true, // ❌ Property 'invalidField' does not exist on type 'Post'
} satisfies FieldMask<Post>,
reason: 'user-access'
}
IDE Support
TypeScript provides excellent IDE support with AuthzKit:
Autocomplete
Get smart autocomplete for action names, properties, and field masks:
policy.check('v|') // IDE suggests: viewPost, viewUser, etc.
policy.check('editPost', {
subject: user,
resource: post,
changes: {
t| // IDE suggests: title
c| // IDE suggests: content
// published not suggested (not in allowed fields)
}
})
Type Hints
See rich type information in your IDE:
// Hover over 'decision' to see full type information
const decision = policy.checkDetailed('viewPost', {
subject: user,
resource: post
})
// IDE shows:
// const decision: {
// allow: boolean;
// reason: string;
// readMask?: FieldMask<Post>;
// writeMask?: FieldMask<Post>;
// attrs?: Record<string, any>;
// }
Inline Error Messages
Get immediate feedback about type errors:
policy.check('editPost', {
subject: user,
resource: post,
changes: { role: 'admin' } // Red underline with error message
// ^^^^ Property 'role' does not exist on type 'Partial<Pick<Post, "title" | "content">>'
})
Generic Type Helpers
AuthzKit provides utility types for common patterns:
FieldMask Type
Ensure field masks match your resource types:
import type { FieldMask } from '@authzkit/core'
// Type-safe field mask
const userReadMask: FieldMask<User> = {
id: true,
name: true,
email: true,
// password: true, // ❌ TypeScript error if password doesn't exist on User
}
// Use with satisfies for better error messages
const readMask = {
id: true,
title: true,
invalidField: true, // ❌ Clear error message
} satisfies FieldMask<Post>
Subject and Resource Extraction
Extract types from action definitions:
type ViewPostSubject = Actions['viewPost']['subject'] // User
type ViewPostResource = Actions['viewPost']['resource'] // Post
// Use in function signatures
function handlePostView(
subject: ViewPostSubject,
resource: ViewPostResource
) {
return policy.check('viewPost', { subject, resource })
}
Advanced Type Patterns
Conditional Types
Create sophisticated type relationships:
type WriteActions = {
[K in keyof Actions]: Actions[K] extends { changes: any } ? K : never
}[keyof Actions]
// WriteActions = 'editPost' | 'createPost' | ...
function validateWriteAction<T extends WriteActions>(
action: T,
request: Actions[T]
) {
// TypeScript knows 'request' has a 'changes' property
return policy.check(action, request)
}
Branded Types
Use branded types for additional safety:
type UserId = string & { __brand: 'UserId' }
type PostId = string & { __brand: 'PostId' }
type User = {
id: UserId
name: string
}
type Post = {
id: PostId
authorId: UserId // Ensures relationship consistency
title: string
}
// TypeScript prevents mixing up IDs
const userId: UserId = 'user-123' as UserId
const postId: PostId = 'post-456' as PostId
// This would be a TypeScript error:
// const user: User = { id: postId, name: 'John' } // ❌ PostId not assignable to UserId
Template Literal Types
Create strongly-typed action names:
type ResourceType = 'post' | 'user' | 'comment'
type ActionType = 'view' | 'edit' | 'delete'
type ActionName = `${ActionType}${Capitalize<ResourceType>}`
// ActionName = 'viewPost' | 'editPost' | 'deletePost' | 'viewUser' | ...
const actions = defineActions({
viewPost: action<{...}>(),
editPost: action<{...}>(),
// TypeScript ensures all combinations are defined
} satisfies Record<ActionName, any>)
Runtime Type Safety
While TypeScript provides compile-time safety, you may want runtime validation:
Zod Integration
Use Zod for runtime type validation:
import { z } from 'zod'
const UserSchema = z.object({
id: z.string(),
role: z.enum(['admin', 'user', 'moderator']),
tenantId: z.string()
})
const PostSchema = z.object({
id: z.string(),
title: z.string(),
content: z.string(),
authorId: z.string(),
tenantId: z.string(),
published: z.boolean()
})
// Runtime validation with type inference
function checkPostAccess(subjectData: unknown, resourceData: unknown) {
const subject = UserSchema.parse(subjectData) // Throws if invalid
const resource = PostSchema.parse(resourceData)
// Now TypeScript knows the exact types
return policy.check('viewPost', { subject, resource })
}
Input Validation
Validate API inputs before authorization:
app.post('/api/posts/:id/edit', async (req, res) => {
try {
// Validate input shapes
const subject = UserSchema.parse(req.user)
const resource = PostSchema.parse(await getPost(req.params.id))
const changes = PostChangesSchema.parse(req.body)
const decision = policy.checkDetailed('editPost', {
subject,
resource,
changes
})
if (!decision.allow) {
return res.status(403).json({ error: decision.reason })
}
// Apply changes with type safety
const updatedPost = await updatePost(resource.id, changes)
res.json(applyReadMask(updatedPost, decision.readMask))
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: 'Invalid input', details: error.issues })
}
throw error
}
})
Testing Type Safety
Test that your types work correctly:
describe('Type Safety', () => {
it('enforces correct action parameters', () => {
// This should compile
const validRequest = {
subject: mockUser,
resource: mockPost
} satisfies typeof actions['viewPost']
expect(validRequest).toBeDefined()
// This test verifies TypeScript compilation
const allowed = policy.check('viewPost', validRequest)
expect(typeof allowed).toBe('boolean')
})
it('provides correct decision types', () => {
const decision = policy.checkDetailed('viewPost', {
subject: mockUser,
resource: mockPost
})
// TypeScript ensures these properties exist
expect(typeof decision.allow).toBe('boolean')
expect(typeof decision.reason).toBe('string')
if (decision.readMask) {
// TypeScript knows readMask has Post fields
expect(typeof decision.readMask.id).toBe('boolean')
expect(typeof decision.readMask.title).toBe('boolean')
}
})
})
Common Type Patterns
Optional Resource for List Actions
Handle list actions that don't have a specific resource:
export const actions = defineActions({
listPosts: action<{
subject: User;
resource?: {} // Optional for list operations
}>(),
viewPost: action<{
subject: User;
resource: Post // Required for specific resource
}>()
})
// Usage
policy.check('listPosts', { subject: user }) // ✅ No resource needed
policy.check('viewPost', { subject: user, resource: post }) // ✅ Resource required
Union Types for Multi-Resource Actions
Handle actions that work with multiple resource types:
type Content = Post | Comment | Article
export const actions = defineActions({
moderateContent: action<{
subject: User;
resource: Content;
action: 'approve' | 'reject' | 'flag';
}>()
})
// TypeScript handles union types correctly
const canModerate = policy.check('moderateContent', {
subject: moderator,
resource: post, // Post | Comment | Article
action: 'approve'
})
Contextual Information
Add typed context for complex scenarios:
export const actions = defineActions({
downloadFile: action<{
subject: User;
resource: File;
context?: {
ipAddress: string;
userAgent: string;
requestId: string;
}
}>()
})
// TypeScript validates context structure
policy.check('downloadFile', {
subject: user,
resource: file,
context: {
ipAddress: req.ip,
userAgent: req.get('User-Agent'),
requestId: req.id
}
})
Performance Considerations
TypeScript compilation can slow down with complex types:
Type Complexity
Keep type definitions manageable:
// ✅ Simple, fast types
type User = {
id: string
role: 'admin' | 'user'
tenantId: string
}
// ❌ Overly complex types (slower compilation)
type ComplexUser = {
[K in 'id' | 'role' | 'tenant']: K extends 'id' ? string
: K extends 'role' ? 'admin' | 'user' | 'moderator' | 'guest' | 'superuser'
: K extends 'tenant' ? { id: string; name: string; plan: 'free' | 'pro' | 'enterprise' }
: never
}
Incremental Compilation
Structure your types for optimal TypeScript performance:
// ✅ Separate type definitions into modules
// types/user.ts
export type User = { /* ... */ }
// types/post.ts
export type Post = { /* ... */ }
// actions/index.ts
import type { User } from '../types/user'
import type { Post } from '../types/post'
export const actions = defineActions({
viewPost: action<{ subject: User; resource: Post }>()
})
Next Steps
- Explore Integration Examples to see type safety in real applications
- Check out the Getting Started guide to implement AuthzKit
- Learn about Field-Level Permissions for granular type-safe data access