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:

src/example.tsTypeScript
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:

deps-example.tsTypeScript
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 Database

Route 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.

src/controllers/users.tsTypeScript
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:

type-utils.tsTypeScript
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:

Files
src/middleware/auth.tsTypeScript
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:

features.tsTypeScript
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:

validation-example.tsTypeScript
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

UtilityPurposePackage
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.

cached-service.tsTypeScript
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.