Skip to content

FlameWright and Runtime Type Safety

Summary

FlameWright is a toolkit for building runtime type guards. Type guards are simple functions that check if a value matches an expected type. They enable type safety in environments (like GAS) where TypeScript's compile-time checks don't run.

JavaScript
// Use a type guard to check before operating
const isNumber = (value) => typeof value === 'number' && !Number.isNaN(value);

if (isNumber(cell)) {
  console.log(cell + 1); // safe to use as number
}

FlameWright provides predefined guards (FlameGuards) and factories for building complex ones.

Philosophy: Type Safety Without Compilation

In standard Google Apps Script, you can't rely on types:

JavaScript
// Standard GAS: no compile-time checking
function processAge(age) {
  const next = age + 1; // age could be a string "25" — code will fail at runtime
}

TypeScript would catch this, but GAS doesn't use TypeScript at runtime. Values that come from the outside world (sheets, APIs, user input) have unknown types.

The Solution: Runtime Type Guards

Instead of trusting types, check them at runtime:

JavaScript
// Define a type guard function
const isNumber = (v) => typeof v === 'number' && !Number.isNaN(v);

function processAge(value) {
  if (isNumber(value)) {
    const next = value + 1; // safe to use as number
    console.log(next);
  } else {
    console.log('not a number');
  }
}

The guard function:

  • Takes unknown as input
  • Returns true if the value matches the type
  • Returns false otherwise
  • Narrows the type so TypeScript knows it's safe

How It Works: Three Building Blocks

1. FlameGuards — Predefined Guards

FlameGuards are ready-to-use guards for common types:

JavaScript
gaslamp.FlameGuards.isString(value)   // string
gaslamp.FlameGuards.isNumber(value)   // number (not NaN)
gaslamp.FlameGuards.isBoolean(value)  // boolean
gaslamp.FlameGuards.isArray(value)    // array
gaslamp.FlameGuards.isMap(value)      // Map
gaslamp.FlameGuards.isValidDate(value) // valid Date (not Invalid Date)
gaslamp.FlameGuards.isTable(value)    // 2D array (what GAS getValues() returns)

Use them directly:

JavaScript
if (gaslamp.FlameGuards.isNumber(cell)) {
  console.log(cell * 2);
}

Or compose them:

JavaScript
const isNumberOrString = (v) =>
  gaslamp.FlameGuards.isNumber(v) || gaslamp.FlameGuards.isString(v);

2. FlameWright — Parameterized Factories

FlameWright is the "craftsman" that builds complex validators. It provides factories for common patterns:

Array validator — "array of numbers"

JavaScript
const isNumbers = gaslamp.FlameWright.arrayOf(gaslamp.FlameGuards.isNumber);
isNumbers([1, 2, 3]);     // true
isNumbers([1, "2", 3]);   // false

Enum validator — "must be one of these values"

JavaScript
const isStatus = gaslamp.FlameWright.enumOf(['active', 'inactive', 'pending']);
isStatus('active');  // true
isStatus('unknown'); // false

Range validator — "number between min and max"

JavaScript
const isAge = gaslamp.FlameWright.inRange(0, 120);
isAge(25);  // true
isAge(150); // false

Nullable validator — "this type or null"

JavaScript
const isOptionalString = gaslamp.FlameWright.nullable(gaslamp.FlameGuards.isString);
isOptionalString('hello'); // true
isOptionalString(null);    // true
isOptionalString(123);     // false

Union validator — "either this type or that type"

JavaScript
const isStringOrNumber = gaslamp.FlameWright.anyOf([
  gaslamp.FlameGuards.isString,
  gaslamp.FlameGuards.isNumber,
]);
isStringOrNumber('hello'); // true
isStringOrNumber(42);      // true
isStringOrNumber(true);    // false

3. FlameRecord — Schema Objects

A schema object pairs property names with guards:

JavaScript
const userSchema = {
  name: gaslamp.FlameGuards.isString,
  age: gaslamp.FlameGuards.isNumber,
  email: (v) => gaslamp.FlameGuards.isString(v) && v.includes('@'),
};

// Build a validator for the whole object
const isUser = gaslamp.FlameWright.fromSchema(userSchema);
isUser({ name: 'Alice', age: 30, email: 'alice@example.com' }); // true
isUser({ name: 'Bob', age: '25', email: 'bob@example.com' });   // false (age is string)

GAS Reality: Why Object.prototype.toString

gaslamp uses Object.prototype.toString instead of instanceof for type checking:

JavaScript
// ❌ DON'T: instanceof fails across GAS script boundaries
if (value instanceof Date) { ... }

// ✅ DO: uses toString tag, works across boundaries
Object.prototype.toString.call(value) === "[object Date]"

Why? In GAS, scripts can be compiled and executed separately. An object's constructor might not match what you expect. Using toString is more reliable.

This is handled transparently in FlameGuards.isValidDate and other guards, so you don't need to worry about it.

Integration with FlameFrame

The real power of type guards is validation. FlameFrame uses schemas to validate data:

JavaScript
// Define the schema
const schema = {
  name: gaslamp.FlameGuards.isString,
  age: gaslamp.FlameGuards.isNumber,
};

// Validate a DataFrame
const { passed, failed } = gaslamp.FlameFrame.from(df, schema);

// passed: only rows that match
// failed: rows with type errors

This is where gaslamp's type safety comes from: schemas guard every cell.

Building Custom Validators

For complex logic, write custom guards:

JavaScript
// Simple: inline
const isEven = (v) => gaslamp.FlameGuards.isNumber(v) && v % 2 === 0;

// Complex: using multiple checks
const isValidEmail = (v) =>
  gaslamp.FlameGuards.isString(v) &&
  v.includes('@') &&
  v.includes('.');

// Composable: build from smaller guards
const isAdultInUSA = (row) =>
  gaslamp.FlameGuards.isNumber(row.age) &&
  row.age >= 18 &&
  gaslamp.FlameGuards.isString(row.country) &&
  row.country === 'USA';

When composing, think in terms of narrowing: each check should narrow the type more.

Assertion Helpers

gaslamp also provides assertion helpers for when you want to fail fast:

JavaScript
// ensure: returns boolean (soft check)
if (gaslamp.ensure(value, gaslamp.FlameGuards.isString)) {
  // value is definitely a string
}

// assert: throws if check fails (hard check)
gaslamp.assert(value, gaslamp.FlameGuards.isNumber);
// If value is not a number, throws an error

// explain: generates a human-readable error message
const error = gaslamp.explain(value, gaslamp.FlameGuards.isString);
// Returns: "expected string, got number (123)"

Use assert during initialization to catch errors early. Use ensure to guard against unexpected data gracefully.

Performance Note

Type guards are functions called at runtime, so they have a small cost. However:

  • FlameGuards use fast checks (typeof, Array.isArray, toString)
  • Guards are evaluated once during validation, not repeatedly
  • The cost is negligible compared to other DataFrame operations

Don't worry about guard performance. Correctness is more important.

When to Validate

Validate input:

JavaScript
const df = gaslamp.BareFrame.fromSheet(sheet);
const { passed, failed } = gaslamp.FlameFrame.from(df, schema);

Validate intermediate results:

JavaScript
const filtered = df.filter(...);
const { passed, failed } = gaslamp.FlameFrame.from(filtered, schema);

Validate output before returning:

JavaScript
const result = transform(df);
const { passed, failed } = gaslamp.FlameFrame.from(result, schema);
return passed;

Don't over-validate: If data has already been validated and hasn't changed, skip re-validation.