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 { defineService } from "@justscale/core";
import { UserService } from "./services/user-service";
// InstanceOf: Extract instance type from a service token
type UserServiceInstance = InstanceOf<typeof UserService>;
// => { get: (ref: Ref<User>) => 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 defineService
class MyService extends defineService({
inject: { users: UserService, db: DatabaseService },
factory: (deps) => {
// deps has type ResolvedDeps<{ users: ..., db: ... }>
deps.users.get(User.ref`123`);
return { /* ... */ };
},
}) {}ExtractDeps<T> and ExtractAllDeps<T>
Extract direct and transitive dependencies from services:
import type { ExtractDeps, ExtractAllDeps } from "@justscale/core";
import { defineService } from "@justscale/core";
// Database has no dependencies
class Database extends defineService({ inject: {}, factory: () => ({}) }) {}
// UserService depends on Database
class UserService extends defineService({
inject: { db: Database },
factory: ({ db }) => ({}),
}) {}
// AuthService depends on UserService (and transitively on Database)
class AuthService extends defineService({
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 { createFeatureBuilder } from "@justscale/core";
import { ModelRepository } from "@justscale/core/models";
// FeatureToken: result of createFeatureBuilder()...provides()
// Tracks required tokens and provided tokens at the type level
const AuthFeature = createFeatureBuilder()
.name('auth')
.requires(ModelRepository.of(User))
.requires(ModelRepository.of(Session))
.requires(AbstractEmailSender)
.provides((b) => b
.add(PasswordService)
.add(UserService)
.add(SessionService)
.add(AuthController)
);
// AuthFeature is a FeatureToken<TRequires, TProvides>
// TypeScript tracks what it needs and what it provides:
// TRequires = [ModelRepository<User>, ModelRepository<Session>, AbstractEmailSender]
// TProvides = [PasswordService, UserService, SessionService, AuthController]
// Features that require other features get their provides available
const AdminFeature = createFeatureBuilder()
.name('admin')
.requires(AuthFeature) // UserService, SessionService, etc. now available
.provides((b) => b
.add(AdminService) // Can depend on AuthFeature's services
.add(AdminController)
);
// In the app, dependencies are checked at compile time
import JustScale from "@justscale/core";
const app = JustScale()
.add(AuthFeature)
.add(AdminFeature) // Type error if AuthFeature is missing
.build();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 JustScale, { defineService, createController } from "@justscale/core";
import { Get } from '@justscale/http';
class Database extends defineService({
inject: {},
factory: () => ({ query: async (sql: string) => [] }),
}) {}
class UserService extends defineService({
inject: { db: Database },
factory: ({ db }) => ({ findAll: async () => [] }),
}) {}
const UsersController = createController("/users", {
inject: { users: UserService },
routes: (services) => ({
list: Get('/').handle(({ res }) => res.json({ users: [] })),
}),
});
// RequiresSatisfied (compile-time): builder rejects incomplete graphs
const badApp = JustScale()
.add(UsersController) // type error: UserService (and Database) not provided yet
.build();
// Correct: add dependencies before the dependents
const goodApp = JustScale()
.add(Database)
.add(UserService)
.add(UsersController)
.build();
// 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 { defineService } 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 defineService({
inject: config.inject,
factory: (deps) => {
const instance = config.factory(deps);
// Wrap methods with caching logic
return new Proxy(instance, { /* ... */ });
},
});
}
// Use it
class Database extends defineService({
inject: {},
factory: () => ({
query: async (sql: string, params: any[]) => {
return [{ id: "1", name: "Alice" }];
},
}),
}) {}
const CachedUserService = createCachedService({
inject: { db: Database },
factory: ({ db }) => ({
get: (ref: Ref<User>) => db.query(`SELECT * FROM users WHERE id = ?`, [ref.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.