Skip to content

TypeScript Guide

Build Noorle plugins with TypeScript.

Why TypeScript?

Strengths

  • Type Safety - Catch errors at compile time, not runtime
  • Better IDE Support - IntelliSense, refactoring, and auto-completion
  • Modern JavaScript - Use latest ES features with confidence

Best For

  • Complex plugins with strict data contracts
  • Teams that value maintainability and refactoring
  • Developers who prefer compile-time error checking

Trade-offs

  • Additional compilation step during development
  • More setup complexity than JavaScript

Quick Start

Prerequisites

bash
# Install Noorle CLI
curl -L cli.noorle.dev | sh

Create Plugin

bash
# Initialize from template
noorle plugin init my-plugin --template typescript
cd my-plugin

# Install dependencies (TypeScript toolchain, wasm tools)
noorle plugin prepare

# Build and deploy
noorle plugin publish

Minimal Example

typescript
interface ProcessResult {
  processed: boolean;
  timestamp: string;
  data: any;
}

export function process(input: string): string {
  if (!input) {
    throw new Error("Input cannot be empty");
  }

  const result: ProcessResult = {
    processed: true,
    timestamp: new Date().toISOString(),
    data: JSON.parse(input)
  };

  return JSON.stringify(result, null, 2);
}

Project Structure

tree
my-plugin/
├── app.ts                # Main implementation
├── wit/world.wit         # API definition
├── tsconfig.json         # TypeScript configuration
├── package.json          # Language dependencies
├── build.sh              # Build script
├── prepare.sh            # Dependency installer
├── noorle.yaml           # Plugin config (optional)
└── dist/                 # Build output
    ├── plugin.wasm       # Compiled component
    └── *.npack           # Deployment archive

File Descriptions

FilePurpose
app.tsPlugin implementation
wit/world.witWebAssembly interface definition
tsconfig.jsonTypeScript compiler configuration
build.shCompilation and component creation
prepare.shInstalls TypeScript toolchain and wasm tools

Implementation Guide

Basic Plugin Structure

typescript
interface Config {
  mode: 'development' | 'production';
  verbose: boolean;
}

interface InputData {
  id: string;
  payload: unknown;
}

// Type-safe error handling
type Result<T, E = string> =
  | { success: true; data: T }
  | { success: false; error: E };

export function process(input: string): string {
  const config: Config = {
    mode: 'development',
    verbose: true
  };

  const data = parseInput(input);
  const result = processData(data, config);

  return JSON.stringify(result);
}

function parseInput(input: string): InputData {
  if (!input.trim()) {
    throw new Error("Input cannot be empty");
  }

  try {
    return JSON.parse(input) as InputData;
  } catch (error) {
    throw new Error(`Invalid JSON: ${error.message}`);
  }
}

Working with WIT

Define your API in wit/world.wit:

wit
package example:my-plugin;

world my-plugin-component {
    /// Function documentation becomes tool description
    export process: func(input: string) -> result<string, string>;
}

TypeScript-specific binding:

typescript
// WIT interface automatically generates TypeScript bindings
export function process(input: string): string {
  // Implementation must match WIT signature exactly
  return processData(input);
}

Error Handling

typescript
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function parseJsonSafe<T>(input: string): Result<T> {
  try {
    const value = JSON.parse(input) as T;
    return { ok: true, value };
  } catch (error) {
    return {
      ok: false,
      error: error instanceof Error ? error : new Error('Unknown parsing error')
    };
  }
}

export function process(input: string): string {
  const parseResult = parseJsonSafe<InputData>(input);

  if (!parseResult.ok) {
    throw new Error(`Processing failed: ${parseResult.error.message}`);
  }

  // TypeScript knows parseResult.value is available here
  return processValidData(parseResult.value);
}

Environment Variables

typescript
import { getEnvironment } from "wasi:cli/[email protected]";

interface EnvConfig {
  apiKey?: string;
  debug: boolean;
  maxConnections: number;
}

function loadConfig(): EnvConfig {
  const env = getEnvironment();
  const envMap = new Map(env);

  return {
    apiKey: envMap.get('API_KEY'),
    debug: envMap.get('DEBUG') === 'true',
    maxConnections: parseInt(envMap.get('MAX_CONNECTIONS') || '10', 10)
  };
}

JSON Processing

typescript
interface UserData {
  id: number;
  name: string;
  email: string;
  metadata?: Record<string, any>;
}

function validateUser(data: unknown): data is UserData {
  return (
    typeof data === 'object' &&
    data !== null &&
    'id' in data &&
    'name' in data &&
    'email' in data &&
    typeof (data as any).id === 'number' &&
    typeof (data as any).name === 'string' &&
    typeof (data as any).email === 'string'
  );
}

export function processUser(input: string): string {
  const parsed = JSON.parse(input);

  if (!validateUser(parsed)) {
    throw new Error("Invalid user data format");
  }

  // TypeScript now knows 'parsed' is UserData
  const result = {
    ...parsed,
    processedAt: new Date().toISOString(),
    valid: true
  };

  return JSON.stringify(result, null, 2);
}

HTTP Requests (Optional)

Required WIT imports:

rust
import wasi:http/types@0.2.0;
import wasi:http/outgoing-handler@0.2.0;

Implementation:

typescript
import { OutgoingRequest } from "wasi:http/[email protected]";

interface ApiResponse<T> {
  data: T;
  status: number;
  error?: string;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  try {
    // HTTP client implementation with types
    const response = await makeRequest(url);

    return {
      data: response.data as T,
      status: response.status
    };
  } catch (error) {
    return {
      data: null as T,
      status: 500,
      error: error.message
    };
  }
}

Required permissions:

yaml
permissions:
  network:
    allow:
      - host: "api.example.com"

Testing

Local Testing with Wasmtime

bash
# Build the plugin
noorle plugin build

# Test with WAVE format
wasmtime run --invoke 'process("test input")' dist/plugin.wasm
wasmtime run --invoke 'add(1, 2)' dist/plugin.wasm
wasmtime run --invoke 'greet("World")' dist/plugin.wasm

# With environment variables
wasmtime run --env KEY=value --invoke 'process("test")' dist/plugin.wasm

Language-Specific Features

Generic Functions

typescript
function processArray<T, U>(
  items: T[],
  processor: (item: T, index: number) => U,
  filter?: (item: T) => boolean
): U[] {
  const filteredItems = filter ? items.filter(filter) : items;
  return filteredItems.map(processor);
}

interface User {
  id: string;
  name: string;
  active: boolean;
}

export function formatUsers(input: string): string {
  const users: User[] = JSON.parse(input);

  const formatted = processArray(
    users,
    (user, index) => `${index + 1}. ${user.name} (${user.id})`,
    user => user.active
  );

  return JSON.stringify(formatted);
}

Type Guards and Validation

typescript
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function isValidConfig(obj: unknown): obj is Config {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'mode' in obj &&
    'verbose' in obj &&
    (obj as any).mode === 'development' || (obj as any).mode === 'production'
  );
}

export function configure(input: string): string {
  const config = JSON.parse(input);

  if (!isValidConfig(config)) {
    throw new Error("Invalid configuration format");
  }

  // TypeScript knows config is properly typed here
  return `Configured in ${config.mode} mode`;
}

Build Configuration

Dependencies

tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./",
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Build Optimization

Optimize for WebAssembly output in tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ES2020",
    "incremental": true,
    "tsBuildInfoFile": "./dist/.tsbuildinfo"
  }
}

Build Process

bash
# Build your plugin (handles compilation, optimization, packaging)
noorle plugin build

The build process:

  1. Compiles TypeScript to JavaScript with type checking
  2. wkg fetches WIT dependencies from imports
  3. Creates component with TypeScript toolchain
  4. Optimizes output
  5. Packages into .npack archive

Troubleshooting

Common Issues

Build Fails

Problem: TypeScript compilation errors

Solution:

bash
# Check TypeScript version (should be 5.0+)
npx tsc --version

# Run type checking only
npm run typecheck

# Fix type errors before building

Runtime Error

Problem: Type assertion failures at runtime

Solution:

typescript
// Use type guards instead of type assertions
function isValidData(data: unknown): data is MyDataType {
  return typeof data === 'object' && data !== null && 'requiredField' in data;
}

if (!isValidData(parsedData)) {
  throw new Error("Invalid data format");
}

TypeScript-Specific Issue

Problem: Cannot find WASI module declarations

Solution:

bash
# Create or update type declarations
echo 'declare module "wasi:cli/[email protected]" {
  export function getEnvironment(): Array<[string, string]>;
}' > wasi.d.ts

Debugging

Enable debug output:

bash
DEBUG=1 noorle plugin build

View WASM exports:

bash
wasm-tools component wit dist/plugin.wasm

Performance

Optimization tips:

  • Use strict TypeScript settings for better optimization
  • Avoid large utility libraries in favor of specific implementations
  • Use specific imports instead of namespace imports

Best Practices

Do's

  • ✅ Use strict TypeScript compiler options
  • ✅ Define interfaces for all data structures
  • ✅ Implement type guards for runtime validation
  • ✅ Leverage TypeScript's type inference

Don'ts

  • ❌ Use 'any' type unless absolutely necessary
  • ❌ Skip type checking with ts-ignore comments
  • ❌ Import Node.js-specific modules

Security

  • Validate all inputs
  • Use minimal permissions
  • Never log sensitive data
  • Sanitize error messages

Advanced Topics

Custom WASI Interfaces

rust
world my-plugin-component {
    import my-custom:interface/types@1.0.0;
    export process: func(input: string) -> result<string, string>;
}
typescript
// Declare custom interface types
declare module "my-custom:interface/[email protected]" {
  export interface CustomType {
    field: string;
  }
  export function customFunction(data: CustomType): string;
}

import { customFunction, CustomType } from "my-custom:interface/[email protected]";

export function useCustomInterface(input: string): string {
  const data: CustomType = JSON.parse(input);
  return customFunction(data);
}

Performance Monitoring

typescript
interface PerformanceMetrics {
  startTime: number;
  endTime: number;
  duration: number;
  memoryUsed?: number;
}

function withPerformanceTracking<T>(
  operation: () => T,
  name: string
): { result: T; metrics: PerformanceMetrics } {
  const startTime = Date.now();

  const result = operation();

  const endTime = Date.now();
  const metrics: PerformanceMetrics = {
    startTime,
    endTime,
    duration: endTime - startTime
  };

  console.log(`Operation ${name} took ${metrics.duration}ms`);

  return { result, metrics };
}

export function processWithMetrics(input: string): string {
  const { result } = withPerformanceTracking(
    () => processData(input),
    'processData'
  );

  return result;
}

Working with Complex Types

typescript
// Union types for flexible APIs
type ProcessingMode = 'sync' | 'async' | 'batch';
type DataFormat = 'json' | 'xml' | 'csv';

interface ProcessingOptions {
  mode: ProcessingMode;
  format: DataFormat;
  validate: boolean;
}

// Conditional types for advanced type manipulation
type ApiResult<T, M extends ProcessingMode> = M extends 'async'
  ? Promise<T>
  : T;

function processWithMode<M extends ProcessingMode>(
  input: string,
  mode: M,
  options: Partial<ProcessingOptions> = {}
): ApiResult<string, M> {
  const config: ProcessingOptions = {
    mode,
    format: 'json',
    validate: true,
    ...options
  };

  if (mode === 'async') {
    return Promise.resolve(processData(input, config)) as ApiResult<string, M>;
  }

  return processData(input, config) as ApiResult<string, M>;
}

Resources

Documentation

Tools

Examples

Next Steps

Continue Learning

Build Your Plugin