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.
// 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:
// 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:
// 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
unknownas input - Returns
trueif the value matches the type - Returns
falseotherwise - 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:
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:
if (gaslamp.FlameGuards.isNumber(cell)) {
console.log(cell * 2);
}
Or compose them:
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"
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"
const isStatus = gaslamp.FlameWright.enumOf(['active', 'inactive', 'pending']);
isStatus('active'); // true
isStatus('unknown'); // false
Range validator — "number between min and max"
const isAge = gaslamp.FlameWright.inRange(0, 120);
isAge(25); // true
isAge(150); // false
Nullable validator — "this type or null"
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"
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:
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:
// ❌ 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:
// 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:
// 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:
// 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:
FlameGuardsuse 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:
const df = gaslamp.BareFrame.fromSheet(sheet);
const { passed, failed } = gaslamp.FlameFrame.from(df, schema);
Validate intermediate results:
const filtered = df.filter(...);
const { passed, failed } = gaslamp.FlameFrame.from(filtered, schema);
Validate output before returning:
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.
Related¶
- FlameFrame and Validation — How schemas are used for validation
- Cell and Primitive Types — Understanding what types can be guarded
- FlameWright API docs — Deep dive into factory methods (future guide)