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 functiongetClassDeclFromDecoratorProp (propAsgnNode :ts .PropertyAssignment ):ts .ClassDeclaration |undefined {if (!propAsgnNode .parent ||!ts .isObjectLiteralExpression (propAsgnNode .parent )) {return;}constobjLitExprNode =propAsgnNode .parent ;if (!objLitExprNode .parent || !ts .isCallExpression (objLitExprNode .parent )) {return;}constcallExprNode =objLitExprNode .parent ;if (!callExprNode .parent || !ts .isDecorator (callExprNode .parent )) {return;}constdecorator =callExprNode .parent ;if (!decorator .parent || !ts .isClassDeclaration (decorator .parent )) {return;}constclassDeclNode =decorator .parent ;returnclassDeclNode ;}
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, RustOption
type
that can be Some
thing or None
thing.
map
ping
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.,
Option
sTypeScript 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
classOpt <T > {constructor(public readonlyvalue :T |undefined) {};map <U >(pred : (v :T ) =>U | undefined):Opt <U > {if (this.value ) return newOpt (pred (this.value ));return newOpt <U >(undefined );}}newOpt (10).map (a => `${a }`).map (s =>s .split ('')).value ; // [1, 0]functiontryDivide (fraction : {num : number,den : number}): number|undefined {if (fraction .den === 0) return;returnfraction .num /fraction .den ;}newOpt ({num : 5,den : 0}).map (tryDivide ).value ; // undefined