Appearance
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
File | Purpose |
---|---|
app.ts | Plugin implementation |
wit/world.wit | WebAssembly interface definition |
tsconfig.json | TypeScript compiler configuration |
build.sh | Compilation and component creation |
prepare.sh | Installs 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:
- Compiles TypeScript to JavaScript with type checking
wkg
fetches WIT dependencies from imports- Creates component with TypeScript toolchain
- Optimizes output
- 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
- TypeScript Compiler - Official TypeScript compiler
- ts-node - TypeScript execution for Node.js
Examples
Next Steps
Continue Learning
- Other Language Guides - Explore Python, TypeScript, JavaScript, Go, Rust
- Configuration - Advanced plugin settings
- Publishing - Deploy your plugin
Build Your Plugin
- Quick Start - Step-by-step guide
- Project Structure - Understanding plugin anatomy
- Platform Overview - How plugins work in Noorle