3 TypeScript SDK
Insidious Fiddler edited this page 2026-02-06 21:04:12 +00:00

TypeScript SDK

Feature flag evaluation for TypeScript and JavaScript applications.

Installation

npm install @hoist/sdk
# or
pnpm add @hoist/sdk
# or
yarn add @hoist/sdk

Quick Start

import { HoistClient } from '@hoist/sdk'

const client = new HoistClient({
  apiKey: 'hoist_live_xxx',
  baseURL: 'https://hoist.example.com',
})

// Wait for initialization
await client.waitForInitialization()

// Evaluate a flag
const enabled = await client.booleanValue('new-feature', false, {
  targetingKey: 'user-123',
  attributes: { plan: 'pro' },
})

if (enabled) {
  // New feature code path
}

// Cleanup when done
client.close()

Configuration

const client = new HoistClient({
  // Required
  apiKey: 'hoist_live_xxx',
  baseURL: 'https://hoist.example.com',

  // Optional
  streaming: true,           // Enable WebSocket streaming (default: true for server keys)
  timeout: 5000,             // Request timeout in ms (default: 5000)

  // Hooks for observability
  hooks: [
    {
      before: async (key, context) => {
        console.log(`Evaluating: ${key}`)
      },
      after: async (key, result) => {
        console.log(`Result: ${key} = ${result.value}`)
      },
      error: async (key, error) => {
        console.error(`Error: ${key}`, error)
      },
    },
  ],

  // Custom logger
  logger: {
    debug: (msg, ...args) => console.debug(msg, ...args),
    info: (msg, ...args) => console.info(msg, ...args),
    warn: (msg, ...args) => console.warn(msg, ...args),
    error: (msg, ...args) => console.error(msg, ...args),
  },
})

API Key Types

Server Keys (hoist_live_* / hoist_test_*)

For Node.js backends and server-side rendering:

  • Full configuration fetched on startup
  • Local evaluation (fast, no network latency per evaluation)
  • Real-time updates via WebSocket streaming
  • Access to all flags
const client = new HoistClient({
  apiKey: 'hoist_live_xxx',  // Server key
  streaming: true,
})

Client Keys

For browser applications where targeting rules should be protected:

  • Server-side evaluation (API call per evaluation)
  • Targeting rules not exposed to client
  • No local flag configuration
const client = new HoistClient({
  apiKey: 'hoist_pk_xxx',  // Client key
})

Evaluation Methods

Boolean

// Simple evaluation
const enabled = await client.booleanValue('dark-mode', false, {
  targetingKey: 'user-123',
})

// With full details
const result = await client.evaluate('dark-mode', false, {
  targetingKey: 'user-123',
})
console.log(result.value, result.reason, result.variantKey)

String

const version = await client.stringValue('api-version', 'v1', {
  targetingKey: 'user-123',
})

Number

const limit = await client.numberValue('rate-limit', 100, {
  targetingKey: 'user-123',
})

JSON

interface FeatureConfig {
  maxItems: number
  enabled: boolean
  allowlist: string[]
}

const config = await client.jsonValue<FeatureConfig>('feature-config', {
  maxItems: 10,
  enabled: false,
  allowlist: [],
}, {
  targetingKey: 'user-123',
})

console.log(config.maxItems)

Evaluation Context

Provide user context for targeting:

const context: EvaluationContext = {
  // Required for consistent targeting and percentage rollouts
  targetingKey: 'user-123',

  // Optional attributes for targeting rules
  attributes: {
    email: 'user@example.com',
    plan: 'enterprise',
    country: 'US',
    betaTester: true,
    accountAge: 365,
    teams: ['engineering', 'platform'],
  },
}

const enabled = await client.booleanValue('feature', false, context)

Evaluation Results

Full evaluation results include:

const result = await client.evaluate('feature', false, context)

result.flagKey     // The evaluated flag key
result.value       // The evaluated value
result.variantKey  // The variant key (if multivariate)
result.reason      // Why this value was returned
result.version     // Flag configuration version
result.error       // Error if evaluation failed

Reasons

Reason Description
DEFAULT No rules matched, using default
TARGETING_MATCH Matched a targeting rule
SEGMENT_MATCH Matched via segment
ROLLOUT Percentage rollout
IDENTITY_OVERRIDE User-specific override
DISABLED Flag is disabled
NOT_FOUND Flag doesn't exist
ERROR Evaluation error

Hooks

Add hooks for logging, metrics, and error tracking:

const client = new HoistClient({
  apiKey: 'hoist_live_xxx',
  baseURL: 'https://hoist.example.com',
  hooks: [
    {
      before: async (key, context) => {
        // Called before evaluation
        console.log(`Evaluating: ${key}`)
      },
      after: async (key, result) => {
        // Called after successful evaluation
        analytics.track('flag_evaluated', {
          flag: key,
          value: result.value,
          reason: result.reason,
        })
      },
      error: async (key, error) => {
        // Called on evaluation error
        errorTracker.capture(error, { flag: key })
      },
    },
  ],
})

Initialization

Waiting for Initialization

const client = new HoistClient(config)

// Wait for the client to be ready
await client.waitForInitialization()

// Now safe to evaluate flags
const enabled = await client.booleanValue('feature', false, context)

Handling Initialization Errors

const client = new HoistClient(config)

try {
  await client.waitForInitialization()
} catch (error) {
  console.error('Failed to initialize Hoist:', error)
  // Client will still work, returning defaults
}

Real-Time Streaming

Server keys automatically connect to WebSocket for real-time updates:

const client = new HoistClient({
  apiKey: 'hoist_live_xxx',
  baseURL: 'https://hoist.example.com',
  streaming: true,  // Default for server keys
})

// Flags are automatically updated when changed in dashboard

Disable Streaming

For environments where WebSocket isn't available:

const client = new HoistClient({
  apiKey: 'hoist_live_xxx',
  baseURL: 'https://hoist.example.com',
  streaming: false,  // Disable streaming
})

Node.js Usage

import { HoistClient } from '@hoist/sdk'

// Initialize once on startup
const flagClient = new HoistClient({
  apiKey: process.env.HOIST_API_KEY!,
  baseURL: process.env.HOIST_URL!,
})

await flagClient.waitForInitialization()

// Use in request handlers
export async function handler(req: Request): Promise<Response> {
  const userId = getUserId(req)

  const newCheckout = await flagClient.booleanValue('new-checkout', false, {
    targetingKey: userId,
    attributes: {
      plan: getUserPlan(userId),
    },
  })

  if (newCheckout) {
    return handleNewCheckout(req)
  }
  return handleLegacyCheckout(req)
}

// Cleanup on shutdown
process.on('SIGTERM', () => {
  flagClient.close()
})

Browser Usage

For browser applications, use client keys to protect targeting rules:

import { HoistClient } from '@hoist/sdk'

const client = new HoistClient({
  apiKey: 'hoist_pk_xxx',  // Client key
  baseURL: 'https://hoist.example.com',
})

// Evaluate flags (server-side evaluation)
const showBanner = await client.booleanValue('promo-banner', false, {
  targetingKey: getCurrentUserId(),
})

if (showBanner) {
  document.getElementById('banner')?.classList.remove('hidden')
}

React Integration

import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
import { HoistClient, EvaluationContext } from '@hoist/sdk'

// Create context
const HoistContext = createContext<HoistClient | null>(null)

// Provider component
export function HoistProvider({
  children,
  apiKey,
  baseURL,
}: {
  children: ReactNode
  apiKey: string
  baseURL: string
}) {
  const [client, setClient] = useState<HoistClient | null>(null)

  useEffect(() => {
    const hoistClient = new HoistClient({ apiKey, baseURL })
    hoistClient.waitForInitialization().then(() => {
      setClient(hoistClient)
    })

    return () => {
      hoistClient.close()
    }
  }, [apiKey, baseURL])

  return (
    <HoistContext.Provider value={client}>
      {children}
    </HoistContext.Provider>
  )
}

// Hook for flag evaluation
export function useFlag(
  key: string,
  defaultValue: boolean,
  context: EvaluationContext
): boolean {
  const client = useContext(HoistContext)
  const [value, setValue] = useState(defaultValue)

  useEffect(() => {
    if (client) {
      client.booleanValue(key, defaultValue, context).then(setValue)
    }
  }, [client, key, defaultValue, context])

  return value
}

// Usage
function MyComponent() {
  const showFeature = useFlag('new-feature', false, {
    targetingKey: userId,
  })

  return showFeature ? <NewFeature /> : <OldFeature />
}

Error Handling

try {
  const enabled = await client.booleanValue('feature', false, context)
  // Use the value
} catch (error) {
  // Handle error
  console.error('Flag evaluation failed:', error)
}

// Or check result for errors
const result = await client.evaluate('feature', false, context)
if (result.error) {
  console.error('Evaluation error:', result.error)
}
// result.value is the default on error

TypeScript Types

import type {
  HoistConfig,
  EvaluationContext,
  EvaluationResult,
  Hook,
  Logger,
  Reason,
} from '@hoist/sdk'

// All types are exported for use in your application
const config: HoistConfig = {
  apiKey: 'hoist_live_xxx',
  baseURL: 'https://hoist.example.com',
}

const context: EvaluationContext = {
  targetingKey: 'user-123',
  attributes: { plan: 'pro' },
}

Best Practices

1. Initialize Once

// Good: Single instance
const client = new HoistClient(config)
export { client }

// Bad: New instance per evaluation
async function getFlag() {
  const client = new HoistClient(config)  // Don't do this!
  return client.booleanValue('feature', false, context)
}

2. Always Provide Targeting Key

// Good: Consistent targeting
await client.booleanValue('feature', false, {
  targetingKey: userId,
})

// Bad: No targeting key
await client.booleanValue('feature', false, {})

3. Handle Cleanup

// Cleanup on shutdown
process.on('SIGTERM', () => client.close())
process.on('SIGINT', () => client.close())

4. Use Type-Safe Defaults

// Good: TypeScript enforces correct default type
const count = await client.numberValue('max-items', 10, context)

// Type error: default must be number
const count = await client.numberValue('max-items', 'ten', context)

Next Steps