Type assertions (as)
Explains the `as` type assertion in CruftScript: a cast from an outside value to a runtime-checkable type inserts a real check that faults on a bad shape, while same-type widening, `as const`, uncheckable targets, and the `as unknown as T` laundering move are all rejected.
as tells the checker to treat a value as a given type. In CruftScript it is restricted to casts it can actually justify, and where the cast could go wrong it inserts a real check that runs at runtime rather than taking your word for it.
expr as Type
Three kinds of cast
Casts of outside values are checked. The admitted case today is a cast of a value entering your program from outside it — typically an unknown — to a concrete target the checker can verify at runtime. That means a target built from primitives, their unions, tuples, arrays, and records, and objects whose properties are themselves checkable. When the cast is admitted, it inserts a real shape-check at the cast site: the value is inspected at runtime and the cast faults with RuntimeTypeAssertionFailed if the shape does not match.
const raw: unknown = parseIncoming(data)
raw as number // ok, plus an inserted runtime check
raw as { id: number } // ok, plus an inserted runtime check of the shape
Same-type widening is not admitted. Unlike TypeScript, a widening cast whose target the source already fits is not currently a free up-cast — the checker routes it to the reject arm rather than treating it as a no-op. as const is rejected the same way (its target is not proven by the source expression).
const n: number = 42
n as (number | string) // error: AssertionOperatorUnsupported
42 as const // error: AssertionOperatorUnsupported
Uncheckable and unrelated casts are rejected. If the target has no way to be checked at runtime, the cast is a compile error. A function type, for instance, has no runtime shape to inspect.
const raw: unknown = getValue()
raw as ((x: number) => number) // error: a function type has no runtime check
No double-cast escape
TypeScript lets you launder any value into any type with value as unknown as T, because unknown sits in the middle and both steps are unchecked. CruftScript closes this. Any target that contains unknown is a compile error, so the whole as unknown as T move is rejected outright. A cast between two unrelated types is likewise refused.
someObj as unknown as HTMLElement // error: the double-cast escape is rejected
Why it works this way
TypeScript's as is unchecked trust: it changes what the checker believes without ever confirming it, and the as unknown as T escape hatch turns that into a way to assert anything at all. Unverified data cast this way flows into typed code and fails somewhere far from the cast. CruftScript keeps as honest by splitting it in two. Where the target is a runtime-checkable concrete type, the cast is turned into a real runtime check that catches a bad value at the cast site; where there is no way to check it — a function type, an unknown-bearing target — it is refused. The narrow admitted surface today is unknown-to-concrete; widening and as const are refused rather than admitted free. There is no version of as that silently trusts.
Limitations
- The target must be checkable to narrow. A down-cast or a cast of an outside value only works when the target is built from primitives and checkable composites. Cast to a function type, or anything else with no runtime shape, and it is rejected.
- No
as unknown as T. Any target mentioningunknownis an error. The classic laundering move does not exist here. - Admitted checked casts run code. Unlike an erased annotation, a checked
asinserts a runtime check that can fault. That is the point, but it means the cast is not free.