TypeScript Utilities
Advanced TypeScript patterns and utilities in JustScale
JustScale leverages TypeScript's advanced type system to provide compile-time guarantees. Understanding these type utilities helps you build more robust applications and create custom abstractions.
Dependency Type Utilities
JustScale provides utilities for working with dependencies and their resolved instances.
InstanceOf<T> and ResolvedDeps<T>
These utilities extract instance types from service tokens and convert dependency maps to resolved instances:
import type { InstanceOf, ResolvedDeps } from "@justscale/core";
import { createService } from "@justscale/core";
import { UserService } from "./services/user-service";
// InstanceOf: Extract instance type from a service token
type UserServiceInstance = InstanceOf<typeof UserService>;
// => { findById: (id: string) => Promise<User>, ... }
// Works with classes too
class DatabaseService {
query(sql: string) { /* ... */ }
}
type DbInstance = InstanceOf<typeof DatabaseService>;
// => DatabaseService
// ResolvedDeps: Convert dependency map to resolved instances
const deps = {
users: UserService,
db: DatabaseService,
};
type Resolved = ResolvedDeps<typeof deps>;
// => {
// users: UserServiceInstance,
// db: DatabaseService,
// }
// Used internally by createService
const MyService = createService({
inject: { users: UserService, db: DatabaseService },
factory: (deps) => {
// deps has type ResolvedDeps<{ users: ..., db: ... }>
deps.users.findById("123");
return { /* ... */ };
},
});ExtractDeps<T> and ExtractAllDeps<T>
Extract direct and transitive dependencies from services:
import type { ExtractDeps, ExtractAllDeps } from "@justscale/core";
import { createService } from "@justscale/core";
// Database has no dependencies
const Database = createService({ inject: {}, factory: () => ({}) });
// UserService depends on Database
const UserService = createService({
inject: { db: Database },
factory: ({ db }) => ({}),
});
// AuthService depends on UserService (and transitively on Database)
const AuthService = createService({
inject: { users: UserService },
factory: ({ users }) => ({}),
});
// ExtractDeps: Get direct dependencies only
type UserServiceDeps = ExtractDeps<typeof UserService>;
// => { db: typeof Database }
// ExtractAllDeps: Get all transitive dependencies
type AllDeps = ExtractAllDeps<typeof AuthService>;
// => typeof UserService | typeof DatabaseRoute Context Types
Route handlers receive a context object with dependencies, parameters, and transport-specific properties. Understanding these types helps with type-safe middleware and handlers.
HandlerContext<TDeps>
The full context type available in route handlers. Combines dependencies, transport context, and built-in properties.
import type { HandlerContext } from "@justscale/core";
import { createController } from "@justscale/core";
import { Get } from '@justscale/http';
import { UserService } from "../services/user-service";
import { AuthService } from "../services/auth-service";
const UsersController = createController("/users", {
inject: { users: UserService, auth: AuthService },
routes: (services) => ({
list: Get("/").handle((ctx) => {
// ctx has type:
// HandlerContext<{ users: UserService, auth: AuthService }>
//
// Which expands to:
// {
// users: UserServiceInstance,
// auth: AuthServiceInstance,
// params: Record<string, string>,
// logger: Logger,
// res: JsonResponse, // from HTTP plugin
// req: IncomingMessage,
// // ... other transport properties
// }
ctx.users.findAll();
ctx.logger.info("Listing users");
ctx.res.json({ users: [] });
}),
}),
});ExtractParams<Path> and Prettify<T>
Type utilities for path parameter extraction and type cleaning:
import type { ExtractParams, Prettify } from "@justscale/core";
import { createController } from "@justscale/core";
import { Get } from '@justscale/http';
// ExtractParams: Extract path parameters from route strings
type Params1 = ExtractParams<"/users/:id">;
// => { id: string }
type Params2 = ExtractParams<"/users/:userId/posts/:postId">;
// => { userId: string, postId: string }
type Params3 = ExtractParams<"/api/v1/items">;
// => {} (no params)
// Used automatically in route handlers
const UsersController = createController('/users', {
routes: () => ({
getOne: Get('/:id').handle(({ params }) => {
params.id; // TypeScript knows this exists!
}),
}),
});
// Prettify: Flatten intersection types for better tooltips
type Complex = { a: string } & { b: number } & { c: boolean };
// Tooltip shows: { a: string } & { b: number } & { c: boolean }
type Clean = Prettify<Complex>;
// Tooltip shows: { a: string, b: number, c: boolean }Middleware Type Utilities
Middleware types help ensure type-safe context accumulation.
Middleware<TIn, TOut> and MiddlewareDef<TAdded, TDeps>
Type-safe middleware with and without dependency injection:
import type { Middleware, MiddlewareDef } from "@justscale/core";
import { createMiddleware } from "@justscale/core";
import { TokenService } from "../services/token-service";
// Simple middleware without dependencies
const parseAuth: Middleware<
{ req: IncomingMessage },
{ user: User }
> = async (ctx) => {
const token = ctx.req.headers.authorization;
const user = await validateToken(token);
return { user };
};
// Middleware with dependency injection
export const AuthMiddleware: MiddlewareDef<
{ user: User },
{ tokens: TokenService }
> = createMiddleware({
inject: { tokens: TokenService },
handler: ({ tokens }) => async (ctx) => {
const user = await tokens.validate(ctx.req.headers.authorization);
return { user };
},
});Feature Type Utilities
Features use advanced types to handle dependency graphs and configuration.
PendingFeature<TConfig> and ResolvedFeatureDeps<T>
Type utilities for working with features and their dependencies:
import type { PendingFeature, ResolvedFeatureDeps } from "@justscale/core";
import { createFeature, createCluster } from "@justscale/core";
// PendingFeature: Represents a feature awaiting configuration
interface AuthConfig {
secretKey: string;
tokenExpiry: number;
}
const AuthFeature = createFeature<AuthConfig>({
config: (cfg) => ({
secretKey: cfg.secretKey,
tokenExpiry: cfg.tokenExpiry ?? 3600,
}),
build: (cfg) => ({
services: [TokenService, SessionService],
controllers: [AuthController],
}),
});
// AuthFeature has type: PendingFeature<AuthConfig>
const app = createCluster({
features: [
AuthFeature({ secretKey: "...", tokenExpiry: 7200 }),
],
});
// ResolvedFeatureDeps: Extract required features
const AdminFeature = createFeature({
requires: { auth: AuthFeature },
build: ({ auth }) => ({
// auth is the resolved AuthFeature instance
services: [AdminService],
controllers: [AdminController],
}),
});
type AdminDeps = ResolvedFeatureDeps<typeof AdminFeature>;
// => { auth: ResolvedFeature<AuthFeature> }Validation Types
JustScale uses type-level validation to catch errors at compile time.
ValidateDeps and ValidateDepsNoConflict
Compile-time validation ensures dependencies are provided and don't conflict:
import { createService, createController, createCluster } from "@justscale/core";
import { Get } from '@justscale/http';
const Database = createService({
inject: {},
factory: () => ({ query: async (sql: string) => [] }),
});
const UserService = createService({
inject: { db: Database },
factory: ({ db }) => ({ findAll: async () => [] }),
});
const UsersController = createController("/users", {
inject: { users: UserService },
routes: (services) => ({
list: Get('/').handle(({ res }) => res.json({ users: [] })),
}),
});
// ValidateDeps: This will compile-time error
const badApp = createCluster({
controllers: [UsersController],
// Missing: UserService and Database!
});
// Error: Property '__dependencies_missing__' is missing
// Correct: All dependencies provided
const goodApp = createCluster({
services: [Database, UserService],
controllers: [UsersController],
});
// ValidateDepsNoConflict: This will error at compile time
const BadController = createController("/bad", {
inject: {
users: UserService,
res: ResponseService, // Error! 'res' is reserved
},
routes: (services) => ({ /* ... */ }),
});
// Error: Property '__context_conflict__' exists with conflicting: 'res'Type Utilities Reference
| Utility | Purpose | Package |
|---|---|---|
InstanceOf<T> | Extract instance type from service token | @justscale/core |
ResolvedDeps<T> | Map dependency tokens to instances | @justscale/core |
ExtractDeps<T> | Get dependencies from service/controller | @justscale/core |
ExtractAllDeps<T> | Get transitive dependencies | @justscale/core |
HandlerContext<T> | Full route handler context type | @justscale/core |
ExtractParams<Path> | Extract params from path string | @justscale/core |
Prettify<T> | Flatten intersection types | @justscale/core |
Building Type-Safe Abstractions
Use these utilities to build your own type-safe wrappers and helpers.
import type { InstanceOf, ResolvedDeps } from "@justscale/core";
import { createService } from "@justscale/core";
// Type-safe service factory wrapper
function createCachedService<
TDeps extends Record<string, ServiceToken>,
TInstance
>(config: {
inject: TDeps;
factory: (deps: ResolvedDeps<TDeps>) => TInstance;
ttl?: number;
}) {
const cache = new Map<string, { value: TInstance; expires: number }>();
return createService({
inject: config.inject,
factory: (deps) => {
const instance = config.factory(deps);
// Wrap methods with caching logic
return new Proxy(instance, { /* ... */ });
},
});
}
// Use it
const Database = createService({
inject: {},
factory: () => ({
query: async (sql: string, params: any[]) => {
return [{ id: "1", name: "Alice" }];
},
}),
});
const CachedUserService = createCachedService({
inject: { db: Database },
factory: ({ db }) => ({
findById: (id: string) => db.query(`SELECT * FROM users WHERE id = ?`, [id]),
}),
ttl: 60000,
});
// Fully typed, with caching!Info
Type Safety First: JustScale's type utilities enable compile-time dependency validation, eliminating entire classes of runtime errors. The framework fails fast during development, not in production.