Type Folding and Options

Sometimes, I work with TypeScript compiler APIs. Because the language semantics are non-trivial, traversing a TypeScript AST almost always involves extensive validation. Here's a particularly specific traversal we use in the Angular Language Service:

ts
/**
* Given a decorator property assignment, return the ClassDeclaration node that
* corresponds to the directive class the property applies to. If the property
* assignment is not on a class decorator, no declaration is returned.
*
* For example,
*
* @Component({
* template: '<div></div>'
* ^^^^^^^^^^^^^^^^^^^^^^^----- property assignment
* })
* class AppComponent {}
* ^^^^^^^^^^^^^^^^^^^^^--------- class declaration
*
* @param propAsgn property assignment
*/
export function getClassDeclFromDecoratorProp(
propAsgnNode: ts.PropertyAssignment): ts.ClassDeclaration|undefined {
if (!propAsgnNode.parent ||
!ts.isObjectLiteralExpression(propAsgnNode.parent)) {
return;
}
const objLitExprNode = propAsgnNode.parent;
if (!objLitExprNode.parent || !ts.isCallExpression(objLitExprNode.parent)) {
return;
}
const callExprNode = objLitExprNode.parent;
if (!callExprNode.parent || !ts.isDecorator(callExprNode.parent)) {
return;
}
const decorator = callExprNode.parent;
if (!decorator.parent || !ts.isClassDeclaration(decorator.parent)) {
return;
}
const classDeclNode = decorator.parent;
return classDeclNode;
}

All branches include a quick return, so in languages with design ideas like functors and monads we could find great solutions to simplifying the type folding done here. I won't explain what these words mean, but if you don't already have an idea for how to simplify this code, here's an approach that I like.

Rust options

First, RustWe could use OCamls. Rust has an Option type that can be Something or Nonething. mapping an Option applies a predicate to a Some or propagates a None.

If the TypeScript API had Rust bindings, I would imagine the code above to look something like

rust
pub fn get_class_decl_from_decorator_prop(
prop_asgn_node: ts::PropertyAssignment,
) -> Option<ts::ClassDeclaration> {
Some(prop_asgn_node)
.map(|asgn| asgn.parent())
.map(|p| match p.kind {
ts::Kind::ObjectLiteralExpression(obj) => Some(obj),
_ => None,
})
.map(|obj| obj.parent())
.map(|p| match p.kind {
ts::Kind::CallExpression(ce) => Some(ce),
_ => None,
})
.map(|ce| ce.parent())
.map(|p| match p.kind {
ts::Kind::Decorator(decor) => Some(decor),
_ => None,
})
.map(|decor| decor.parent())
.map(|p| match p.kind {
ts::Kind::ClassDeclaration(cdecl) => Some(cdecl),
_ => None,
})
}

This is way nicer - declaring the procedure functionally and linearly makes it more readable to me.

Back to TypeScript

Okay, so how do we get this in TypeScript? Well, for a variety of good reasonsMainly because TypeScript is focused on supplementing JavaScript with types, not with more language features that aren't in the standard., OptionsTypeScript does provide an HTML Option, which is distinctly different. will probably never be in the standard library. Optional chaining will help, but won't solve type mapping.

For domains where it is still useful to have Option (and other monad-family) types, there at least one library that can be used. If a library is too much, a lightweight Option can be written very quickly:

ts
class Opt<T> {
constructor(public readonly value: T|undefined) {};
 
map<U>(pred: (v: T) => U | undefined): Opt<U> {
if (this.value) return new Opt(pred(this.value));
return new Opt<U>(undefined);
}
}
 
new Opt(10).map(a => `${a}`).map(s => s.split('')).value; // [1, 0]
 
function tryDivide(fraction: {num: number, den: number}): number|undefined {
if (fraction.den === 0) return;
return fraction.num / fraction.den;
}
new Opt({num: 5, den: 0}).map(tryDivide).value; // undefined
Analytics By visiting this site, you agree to its use of Cloudflare Analytics. No identifiable information is transmitted to Cloudflare. See Cloudflare Analytics user privacy.