DEV Community

Cover image for Typescript: Best Type Checking for the Best Type Safety
JS for ZenStack

Posted on • Updated on

Typescript: Best Type Checking for the Best Type Safety

From C# to Typescript

Before using Typescript, I used C# for many years. Put aside the ecosystem and just from the language point of view, I still think C# is the best language I have ever used, which is mainly because of the robust and powerful type system. It is really a pleasure to write code with good type inference, even for the generics, and very few annoying runtime errors.

Therefore, the moment I heard about Typescript, I knew I would definitely use it seriously for sure.

Why? because of the Hejlsberg

breaking bad

No, not either of the guy above, although the name looks similar. 😆 It is this guy:

Hejlsberg

If you have ever dived into the source code of Typescript, you should be familiar with him as he made the most commits in the repository and is also the original designer of C#, Delphi, and Turbo Pascal.

Hejlsberg-commit

Therefore, I knew Typescript would definitely inherit the good quality from C#. What I was not aware of is that It is more than that.

Exceed

I’m building a new toolkit ZenStack to help people build secured full-stack web apps easily. It has its own DSL(Domain specified Language) ZModel, which I chose the OSS library Langium to implement. It does a good job of parsing the language text to AST (abstract syntax tree), but it doesn't support generating code back from AST reversibly. Therefore I have to do that by myself. Luckily, I have done this using C# before. So to some degree, it’s like translating C# to Typescript for me. 😏

When handling the binary expression, it needs to consider the operator precedence to see whether to generate a parenthesis. So following the old road, the first thing is to define the precedence for all the operators. In C#, it can be done by using a dictionary, in Typescript, we can simply use a const object:



export const BinaryExprOperatorPrecedence  = {
    //LogicalExpr
    '||': 1,
    '&&': 1,
    //EqualityExpr
    '==': 2,
    '!=': 2,
    //ComparisonExpr
    '>': 3,
    '<': 3,
    '>=': 3,
    '<=': 3,
    //TODO: add more operators
};


Enter fullscreen mode Exit fullscreen mode

To make it run e2e first, I only added the most obvious operators and left a TODO there as usual.

Then when handling the binary expression, I need to get the precedence first. The code I was about to write is as below:

code-error-1

An interesting thing happened. As you can see, there was a type error for getting the current precedence from the BinaryExprOperatorPrecedence. How come?

After checking the error message, you will know why:



Element implicitly has an 'any' type because expression of 
type '"!" | "!=" | "&&" | "<" | "<=" | "==" | ">" | ">=" | "?" | 
"^" | "||"' 
can't be used to index type '{ '||': number; '&&': number; 
'==': number; '!=': number; '>': number; '<': number; 
'>=': number; '<=': number; }'.

Property '!' does not exist on type '{ '||': number; '&&': number; 
'==': number; '!=': number; '>': number; '<': number; 
'>=': number; '<=': number; }'.ts(7053)


Enter fullscreen mode Exit fullscreen mode

It turns out that it doesn’t give you the chance to throw the NotImplemented exception at all, you have to define all the operators to pass the type checking. 😂

It might sound like extra work, but think about what will happen if later there is a new kind of operator added by someone unaware of my code. He will immediately see the type-checking error above and know he needs to also add the precedence for that operator here. Isn’t this exactly the benefit of type-checking:

Fail fast, Fail often

How to get that

Simply by checking the type definition of the operator, you will see how simple and intuitive to use a union to represent it:



operator: '!' | '!=' | '&&' | '<' | '<=' | '==' | '>' | '>=' 
| '?' | '^' | '||';


Enter fullscreen mode Exit fullscreen mode

Union type is such a great feature to make a stronger type system because it allows you to build new types out of existing ones using a large variety of operators. I think that’s one reason to make Typescript shines because very few mainstream programming languages have built-in support for union. So far as I know, only Scala and Rust also support it.

Beyond

One experience I would like to share when starting to use Typescript is always to ask: can we fail faster?

Yes, we can! Simply just giving the explicit type for BinaryExprOperatorPrecedence as Record<BinaryExpr['operator'], number>

code-error-2

Then rather than see an error where the BinaryExprOperatorPrecedence is used, you will see an error right away with the below message:



Type '{ '||': number; '&&': number; '==': number; '!=': number; 
'>': number; '<': number; '>=': number; '<=': number; }' 

is missing the following properties from type 

'Record<"!" | "!=" | "&&" | "<" | "<=" | "==" | ">" | ">=" | "?" 

| "^" | "||", number>': "!", "?", "^"ts(2739)


Enter fullscreen mode Exit fullscreen mode

BTW, if you used C# like me before, you might want to define the operator as an Enum. You can still do that here without losing any benefits like the below:



enum Operator {
  Add = "+",
  Subtract = "-",
  Multiply = "*",
  Divide = "/",
}

export const BinaryExprOperatorPrecedence:Record<Operator, number> 
= {...}


Enter fullscreen mode Exit fullscreen mode

Although I don’t see a strong point in doing that since Union has done a great job already.

Attention!

There is an essential prerequisite for all the benefits mentioned, and I think it should be a motto for all the typescript users:

If you want to feel safe, work in the Strict mode


Introducing ZenStack: a toolkit that supercharges Prisma ORM with a powerful access control layer and unleashes its full potential for full-stack development.

Top comments (0)