Juris Types Developer Guide - jurisjs/juris GitHub Wiki
A comprehensive guide to building type-safe applications with the Juris framework, featuring advanced TypeScript definitions, component validation, and enterprise-grade developer experience.
The Juris Types system provides framework-aware TypeScript definitions that understand how Juris works internally. Unlike generic TypeScript, these types know about:
- VDOM structure and element properties
- Reactive patterns and async handling
- Component lifecycle and props
- State management with dot notation
- Event handling with proper signatures
// ❌ Regular JavaScript - Runtime errors
{
dvi: { className: 'typo' }, // Silent failure
UserCard: { invalidProp: true }, // Runtime error
children: [42, null, undefined] // Broken rendering
}
// ✅ With Juris Types - Caught immediately
{
div: { className: 'correct' }, // ✅ Valid HTML element
UserCard: { props: { user } }, // ✅ Typed component props
children: [{ p: { text: 'Hi' } }] // ✅ Valid VDOM elements
}
# Copy the types folder to your project
types/
├── index.d.ts # Core Juris types
├── index.js # JavaScript IntelliSense
├── app.d.ts # Your app-specific types
└── html-helper.js # Types for inline scripts
VS Code (jsconfig.json
):
{
"compilerOptions": {
"checkJs": true,
"allowJs": true,
"baseUrl": ".",
"paths": {
"@types": ["./types/index.d.ts"]
}
},
"include": ["**/*.js", "**/*.d.ts"]
}
/**
* @param {{
* // Define your props here
* }} props
* @param {import('@types').JurisContext} context
* @returns {import('@types').JurisVDOMElement}
*/
export const MyComponent = (props, context) => {
// Full type safety and IntelliSense here!
return {
div: {
text: 'Hello, Juris Types!'
}
};
};
Juris Types provides 3 levels of type safety:
- HTML Elements - Strict validation of HTML tags and properties
- Registered Components - Type-safe custom components
- Framework APIs - Context, state, and lifecycle methods
// HTML Element (built-in)
{
div: { // ✅ Known HTML element
className: 'box', // ✅ Valid HTML property
onClick: () => {} // ✅ Valid event handler
}
}
// Custom Component (registered)
{
UserCard: { // ✅ Registered component
props: { // ✅ Component props
user: userData // ✅ Typed prop
}
}
}
// Invalid Examples
{
dvi: {}, // ❌ Typo in HTML element
UnknownComp: {}, // ❌ Unregistered component
div: { badProp: 1 } // ❌ Invalid HTML property
}
All custom components must be registered in types/app.d.ts
:
declare global {
namespace Juris {
interface RegisteredComponents {
ComponentName: {
props: {
// Define props and their types
};
slots?: {
// Define slots if component supports them
};
};
}
}
}
/**
* @param {{
* title: string,
* count?: number,
* items: Array<{id: string, name: string}>,
* onItemClick?: (item: any) => void,
* disabled?: boolean
* }} props
* @param {import('@types').JurisContext} context
* @returns {import('@types').JurisVDOMElement}
*/
export const ItemList = (props, context) => {
const { title, count = 0, items, onItemClick, disabled = false } = props;
const { getState, setState } = context;
return {
div: {
className: 'item-list',
children: [
{ h2: { text: title } },
{ p: { text: `Total: ${count}` } },
{
ul: {
children: items.map(item => ({
li: {
text: item.name,
onClick: disabled ? undefined : () => onItemClick?.(item)
}
}))
}
}
]
}
};
};
// types/app.d.ts
declare global {
namespace Juris {
interface RegisteredComponents {
ItemList: {
props: {
title: string;
count?: number;
items: Array<{id: string, name: string}>;
onItemClick?: (item: {id: string, name: string}) => void;
disabled?: boolean;
};
};
}
}
}
/**
* @template T
* @param {{
* items: T[],
* renderItem: (item: T, index: number) => import('@types').JurisVDOMElement,
* keyExtractor?: (item: T) => string,
* loading?: boolean,
* emptyMessage?: string
* }} props
* @param {import('@types').JurisContext} context
* @returns {import('@types').JurisVDOMElement}
*/
export const GenericList = (props, context) => {
const { items, renderItem, keyExtractor, loading, emptyMessage } = props;
if (loading) {
return { div: { className: 'loading', text: 'Loading...' } };
}
if (items.length === 0) {
return { div: { className: 'empty', text: emptyMessage || 'No items' } };
}
return {
div: {
className: 'generic-list',
children: items.map((item, index) => {
const key = keyExtractor ? keyExtractor(item) : index.toString();
return {
div: {
key,
children: [renderItem(item, index)]
}
};
})
}
};
};
/**
* @param {{
* title: string,
* isOpen: boolean,
* size?: 'small' | 'medium' | 'large',
* onClose?: () => void
* }} props
* @param {import('@types').JurisContext} context
* @returns {import('@types').JurisVDOMElement}
*/
export const Modal = (props, context) => {
const { title, isOpen, size = 'medium', onClose } = props;
if (!isOpen) return null;
return {
div: {
className: `modal modal-${size}`,
children: [
{
div: {
className: 'modal-header',
children: [
{ h3: { text: title } },
onClose ? {
button: {
className: 'modal-close',
text: '×',
onClick: onClose
}
} : null
].filter(Boolean)
}
},
{
div: {
className: 'modal-body',
children: [] // Will be populated by slots
}
}
]
}
};
};
Register with slots:
Modal: {
props: {
title: string;
isOpen: boolean;
size?: 'small' | 'medium' | 'large';
onClose?: () => void;
};
slots?: {
body?: JurisVDOMElement[];
footer?: JurisVDOMElement[];
};
};
// Elements that primarily contain text
{ h1: { text: 'Heading' } }
{ p: { text: 'Paragraph content' } }
{ span: { text: 'Inline text' } }
{ label: { text: 'Form label' } }
// Elements that contain other elements
{
div: {
className: 'container',
children: [
{ p: { text: 'Child paragraph' } },
{ span: { text: 'Child span' } }
]
}
}
{
input: {
type: 'text',
value: () => getState('inputValue', ''),
placeholder: 'Enter text...',
onChange: (e) => setState('inputValue', e.target.value)
}
}
{
select: {
value: () => getState('selectedValue'),
onChange: (e) => setState('selectedValue', e.target.value),
children: [
{ option: { value: 'a', text: 'Option A' } },
{ option: { value: 'b', text: 'Option B' } }
]
}
}
{
img: {
src: 'image.jpg',
alt: 'Description',
width: 200,
height: 150,
onLoad: () => console.log('Image loaded')
}
}
{
video: {
src: 'video.mp4',
controls: true,
autoplay: false,
onPlay: () => setState('videoPlaying', true)
}
}
Some elements can have either text OR children:
// Text content
{ div: { text: 'Simple text' } }
// Child elements
{
div: {
children: [
{ p: { text: 'Paragraph' } },
{ span: { text: 'Span' } }
]
}
}
// ❌ Don't mix them
{
div: {
text: 'Text',
children: [...] // This will be ignored
}
}
{
div: {
// Identification
id: 'unique-id',
className: 'css-classes',
// Accessibility
'aria-label': 'Screen reader text',
'aria-hidden': false,
role: 'button',
tabIndex: 0,
// Data attributes
'data-testid': 'test-identifier',
'data-value': 'custom-data',
// Styling
style: {
color: 'red',
backgroundColor: 'blue',
padding: '10px'
},
// Events (see Event Handling section)
onClick: (e) => {},
onMouseOver: (e) => {}
}
}
Juris supports reactive patterns where any property can be static, a function, or a Promise.
{
div: {
className: 'static-class',
text: 'Static text',
style: { color: 'red' }
}
}
{
div: {
className: () => getState('theme') === 'dark' ? 'dark-mode' : 'light-mode',
text: () => `Count: ${getState('counter', 0)}`,
style: () => ({
opacity: getState('visible', true) ? 1 : 0
})
}
}
{
div: {
text: fetchUserName(userId), // Returns Promise<string>
children: loadMenuItems(), // Returns Promise<Element[]>
style: computeStyles() // Returns Promise<CSSStyleDeclaration>
}
}
Children can be static, reactive, or async:
{
div: {
// Static children
children: [
{ p: { text: 'Always visible' } }
]
}
}
{
div: {
// Reactive children
children: () => {
const items = getState('items', []);
return items.map(item => ({
div: { text: item.name }
}));
}
}
}
{
div: {
// Async children
children: async () => {
const data = await fetchData();
return data.map(item => ({
div: { text: item.title }
}));
}
}
}
The type system ensures reactive values return the correct types:
{
div: {
// ✅ These are all valid:
text: 'static string',
text: () => 'function returning string',
text: Promise.resolve('promise of string'),
// ❌ These will show errors:
text: () => 42, // Error: number not assignable to string
text: Promise.resolve(true), // Error: boolean not assignable to string
children: [{ p: { text: 'valid' } }], // ✅ Valid element array
children: () => [{ p: { text: 'valid' } }], // ✅ Function returning elements
children: () => [{ badElement: {} }], // ❌ Error: invalid element
}
}
The Juris type system catches errors at multiple levels and provides specific error messages.
// ❌ Typos in HTML elements
{
dvi: { className: 'container' } // Error: Property 'dvi' does not exist
}
{
buton: { text: 'Click' } // Error: Property 'buton' does not exist
}
// ✅ Correct spelling
{
div: { className: 'container' } // ✅ Valid
}
{
button: { text: 'Click' } // ✅ Valid
}
// ❌ Unregistered component
{
UnknownComponent: { // Error: Component 'UnknownComponent' not registered
props: { data: 'test' }
}
}
// ✅ After registering in app.d.ts
{
KnownComponent: { // ✅ Valid registered component
props: { data: 'test' }
}
}
{
div: {
// ❌ Wrong types
className: 123, // Error: number not assignable to string
onClick: 'not a function', // Error: string not assignable to function
// ✅ Correct types
className: 'valid-class', // ✅ String
onClick: () => console.log() // ✅ Function
}
}
{
div: {
children: [
{ p: { text: 'Valid paragraph' } }, // ✅ Valid element
{ invalidElement: {} }, // ❌ Error: invalid element
'Plain text not allowed', // ❌ Error: string not element
42, // ❌ Error: number not element
null // ❌ Error: null not element
]
}
}
// ✅ Correct children
{
div: {
children: [
{ p: { text: 'Valid paragraph' } },
{ span: { text: 'Valid span' } }
]
}
}
{
div: {
// ❌ Function returns wrong type
text: () => getState('count', 0), // Error: number not assignable to string
// ✅ Convert to correct type
text: () => getState('count', 0).toString(), // ✅ String
// ❌ Function returns invalid children
children: () => [
{ validElement: { text: 'ok' } }, // ✅ Valid
{ invalidElement: {} } // ❌ Error: invalid element
]
}
}
The type system validates deeply nested structures:
{
div: {
children: [
{
section: {
children: [
{
article: {
children: [
{ validElement: { text: 'Deep but valid' } }, // ✅ Valid
{ deepTypo: { text: 'Deep typo' } } // ❌ Error caught at any depth
]
}
}
]
}
}
]
}
}
The system provides helpful error messages:
❌ Component 'UserForm' not registered. Add to app.d.ts RegisteredComponents interface.
Example:
declare global {
namespace Juris {
interface RegisteredComponents {
UserForm: {
props: {
// Define your props here
};
};
}
}
}
// Define your app state structure
/**
* @typedef {{
* user: { name: string, email: string, role: 'admin' | 'user' },
* ui: { theme: 'light' | 'dark', sidebar: { open: boolean, width: number } },
* data: { users: User[], loading: boolean, error: string | null }
* }} AppState
*/
/**
* @param {import('@types').JurisContext<AppState>} context
*/
function useTypedState(context) {
const { getState, setState } = context;
// Type-safe state access with dot notation
const userName = getState('user.name', ''); // string
const isLoading = getState('data.loading', false); // boolean
const sidebarOpen = getState('ui.sidebar.open', true); // boolean
// Type-safe state updates
setState('user.role', 'admin'); // ✅ Valid role
setState('ui.theme', 'dark'); // ✅ Valid theme
setState('data.error', 'Something went wrong'); // ✅ Valid error
}
// Subscribe to state changes with proper typing
const unsubscribe = context.subscribe('user.name', (newName, oldName, path) => {
console.log(`User name changed from ${oldName} to ${newName}`);
});
// Subscribe to nested paths
const unsubscribeUI = context.subscribe('ui', (newUI, oldUI, path) => {
console.log('UI state changed:', newUI);
});
// Cleanup subscriptions
unsubscribe();
unsubscribeUI();
// Define typed services
/**
* @typedef {{
* userService: UserService,
* apiClient: ApiClient,
* authService: AuthService
* }} AppServices
*/
/**
* @param {Object} props
* @param {import('@types').JurisContext & { services: AppServices }} context
* @returns {import('@types').JurisVDOMElement}
*/
export const UserManager = (props, context) => {
const { userService, authService } = context.services;
const { getState, setState } = context;
const loadUsers = async () => {
setState('users.loading', true);
try {
const users = await userService.getAll();
setState('users.list', users);
} catch (error) {
setState('users.error', error.message);
} finally {
setState('users.loading', false);
}
};
return {
div: {
children: [
{
button: {
text: 'Load Users',
onClick: loadUsers,
disabled: () => getState('users.loading', false)
}
}
]
}
};
};
/**
* @param {Object} props
* @param {import('@types').JurisContext} context
* @returns {{
* api: {
* loadData: () => Promise<void>,
* saveData: (data: any) => Promise<void>,
* clearData: () => void
* }
* }}
*/
export const DataManager = (props, context) => {
const { getState, setState } = context;
return {
api: {
async loadData() {
setState('loading', true);
try {
const response = await fetch('/api/data');
const data = await response.json();
setState('data', data);
} finally {
setState('loading', false);
}
},
async saveData(data) {
setState('saving', true);
try {
await fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
setState('data', data);
} finally {
setState('saving', false);
}
},
clearData() {
setState('data', null);
}
}
};
};
// Use in components
export const DataComponent = (props, context) => {
const dataManager = context.components.getHeadlessAPI('DataManager');
return {
div: {
children: [
{
button: {
text: 'Load Data',
onClick: () => dataManager.loadData()
}
},
{
button: {
text: 'Clear Data',
onClick: () => dataManager.clearData()
}
}
]
}
};
};
/**
* Higher-order component pattern
* @param {import('@types').JurisComponentFunction} WrappedComponent
* @returns {import('@types').JurisComponentFunction}
*/
export const withLoading = (WrappedComponent) => {
return (props, context) => {
const { getState } = context;
const isLoading = getState('loading', false);
if (isLoading) {
return {
div: {
className: 'loading-wrapper',
text: 'Loading...'
}
};
}
return WrappedComponent(props, context);
};
};
// Usage
const LoadingUserCard = withLoading(UserCard);
.vscode/settings.json
:
{
"typescript.validate.enable": true,
"javascript.validate.enable": true,
"typescript.preferences.checkJs": true,
"javascript.preferences.checkJs": true,
"html.validate.scripts": true,
"typescript.suggest.autoImports": true,
"typescript.preferences.includePackageJsonAutoImports": "auto"
}
jsconfig.json
:
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"allowJs": true,
"checkJs": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@types": ["./types/index.d.ts"],
"@types/*": ["./types/*"],
"@components/*": ["./components/*"],
"@services/*": ["./services/*"]
},
"lib": ["ES2020", "DOM", "DOM.Iterable"]
},
"include": [
"**/*.js",
"**/*.d.ts"
],
"exclude": [
"node_modules",
"dist",
"build"
]
}
Enable strict type checking:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
For gradual adoption:
{
"compilerOptions": {
"strict": false,
"noImplicitAny": false,
"checkJs": true
}
}
// ❌ Error: Property 'dvi' does not exist in type 'Element'
{ dvi: { text: 'typo' } }
// ✅ Fix: Correct the spelling
{ div: { text: 'correct' } }
// ❌ Error: Component 'UserForm' not registered
{ UserForm: { props: {} } }
// ✅ Fix: Add to app.d.ts
declare global {
namespace Juris {
interface RegisteredComponents {
UserForm: { props: {} };
}
}
}
// ❌ Error: Type 'number' is not assignable to type 'string'
{ div: { text: () => getState('count', 0) } }
// ✅ Fix: Convert to string
{ div: { text: () => getState('count', 0).toString() } }
// Check what type TypeScript thinks something is
/**
* @type {import('@types').JurisVDOMElement}
*/
const element = { div: { text: 'test' } };
// Use in hover or go-to-definition
const elementType = /** @type {import('@types').JurisVDOMElement} */ (element);
// Validate component props at runtime
/**
* @param {any} props
* @param {string[]} required
*/
function validateProps(props, required) {
for (const prop of required) {
if (!(prop in props)) {
throw new Error(`Missing required prop: ${prop}`);
}
}
}
export const ValidatedComponent = (props, context) => {
validateProps(props, ['title', 'data']);
// Component implementation...
};
// Runtime state validation
const validateState = (path, value, expectedType) => {
if (typeof value !== expectedType) {
console.warn(`State at '${path}' expected ${expectedType}, got ${typeof value}`);
}
};
// Use in setState
const typedSetState = (path, value) => {
// Add runtime validation
context.setState(path, value);
};
File Structure:
components/
├── ui/ # Basic UI components
│ ├── Button.js
│ ├── Input.js
│ └── Modal.js
├── forms/ # Form-specific components
│ ├── UserForm.js
│ └── SearchForm.js
├── data/ # Data display components
│ ├── UserCard.js
│ ├── DataTable.js
│ └── Chart.js
└── layout/ # Layout components
├── Header.js
├── Sidebar.js
└── Footer.js
Naming Conventions:
- Components:
PascalCase
(e.g.,UserCard
,DataTable
) - Props:
camelCase
(e.g.,isVisible
,onItemClick
) - Event handlers:
on
prefix (e.g.,onClick
,onSubmit
)
Component Props:
// ✅ Good: Specific types
props: {
status: 'pending' | 'approved' | 'rejected';
count: number;
onUpdate: (id: string, data: UserData) => void;
}
// ❌ Avoid: Generic types
props: {
status: string;
count: any;
onUpdate: Function;
}
State Structure:
// ✅ Good: Hierarchical, typed structure
{
ui: {
theme: 'light' | 'dark',
modal: { open: boolean, content: string | null }
},
data: {
users: User[],
loading: boolean,
error: string | null
}
}
// ❌ Avoid: Flat, untyped structure
{
theme: any,
modalOpen: any,
modalContent: any,
users: any,
loading: any
}
Graceful Degradation:
export const SafeComponent = (props, context) => {
const { data, fallback } = props;
// Handle missing data
if (!data || data.length === 0) {
return fallback || { div: { text: 'No data available' } };
}
// Handle errors in rendering
try {
return {
div: {
children: data.map(item => ({
div: { text: item.name || 'Unknown' }
}))
}
};
} catch (error) {
console.error('Rendering error:', error);
return { div: { text: 'Error rendering component' } };
}
};
Type Guards:
/**
* @param {any} value
* @returns {value is User}
*/
function isUser(value) {
return value &&
typeof value.id === 'number' &&
typeof value.name === 'string' &&
typeof value.email === 'string';
}
export const UserComponent = (props, context) => {
const { user } = props;
if (!isUser(user)) {
return { div: { text: 'Invalid user data' } };
}
// Now TypeScript knows user is a valid User
return {
div: {
text: `${user.name} (${user.email})`
}
};
};
Memoization:
export const ExpensiveComponent = (props, context) => {
const { data } = props;
const { getState } = context;
// Cache expensive computations
const processedData = useMemo(() => {
return data.map(item => ({
...item,
processed: expensiveFunction(item)
}));
}, [data]);
return {
div: {
children: processedData.map(item => ({
div: { text: item.processed }
}))
}
};
};
Lazy Loading:
export const LazyComponent = (props, context) => {
const { shouldLoad } = props;
if (!shouldLoad) {
return { div: { text: 'Click to load content' } };
}
return {
div: {
children: async () => {
const data = await loadExpensiveData();
return data.map(item => ({
div: { text: item.title }
}));
}
}
};
};
Component Testing:
// test/components/UserCard.test.js
import { UserCard } from '../components/UserCard.js';
/**
* @param {import('@types').JurisVDOMElement} element
*/
function renderToString(element) {
// Convert VDOM to string for testing
const tagName = Object.keys(element)[0];
const props = element[tagName];
return `<${tagName}>${props.text || ''}</${tagName}>`;
}
describe('UserCard', () => {
test('renders user name and email', () => {
const mockContext = {
getState: () => null,
setState: () => {},
services: {}
};
const props = {
user: { id: 1, name: 'John', email: '[email protected]' },
theme: 'light'
};
const element = UserCard(props, mockContext);
const html = renderToString(element);
expect(html).toContain('John');
expect(html).toContain('[email protected]');
});
});
Component Documentation:
/**
* UserCard - Displays user information in a card format
*
* @example
* ```javascript
* {
* UserCard: {
* props: {
* user: { id: 1, name: 'John', email: 'john@test.com' },
* theme: 'dark',
* onEdit: (user) => console.log('Edit:', user)
* }
* }
* }
* ```
*
* @param {{
* user: {id: number, name: string, email: string, avatar?: string},
* theme?: 'light' | 'dark',
* onEdit?: (user: User) => void,
* onDelete?: (userId: number) => void
* }} props
* @param {import('@types').JurisContext} context
* @returns {import('@types').JurisVDOMElement}
*/
export const UserCard = (props, context) => {
// Implementation...
};
The Juris Types system provides enterprise-grade type safety for modern web applications. By combining TypeScript's static analysis with Juris's reactive patterns, you get:
- Immediate error detection during development
- Framework-aware validation that understands VDOM and components
- Excellent developer experience with full IntelliSense support
- Scalable architecture for teams and large applications
Start with basic component typing and gradually add more sophisticated patterns as your application grows. The type system will guide you toward better code organization and help prevent runtime errors.
Happy coding with Juris Types! 🚀
- [Juris Framework Documentation](https://jurisjs.com/)
- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
- [JSDoc Reference](https://jsdoc.app/)
- [VS Code JavaScript](https://code.visualstudio.com/docs/languages/javascript)
Found an issue or want to improve the types? Contributions are welcome!
- Fork the repository
- Create a feature branch
- Add your improvements
- Submit a pull request
For type-related contributions, please include:
- Updated type definitions
- Usage examples
- Documentation updates
- Test cases (if applicable)