homerss services talks gpg

Don't trust. Verify.

2022-06-14

tl;dr I was disappointed to find out that typescript isn’t very strongly typed. You still have to keep an eye out for any, the generic object, and type coercion.

At one of my more recent gigs, I was thrown into a Typescript environment via cdktf1. Having dabbled in OCaml2 and Haskell previously I was looking forward to having a strong, statically typed language to define infrastructure with. Two of my biggest gripes with many configuration management and IaC solutions are a lack of schema enforcement and the heavy use of templating. 3

Having a full type system allows for some really nice things like this.

type domains = 'pallissard.net' | 'matt.pallissard.net';
export type validEmail = `${string}@${domains}`;

export interface UserConfig {
  email: validEmail;
  firstName: string;
  lastName: string;

This affords one the opportunity to;

  1. Separate their code from their configuration
  2. Use the type system as a DSL of sorts for declaring configuration. Handling schema enforcement at build time.

Exactly what I would hope for from a language with a strong type system. However, I immediately ran into an eyebrow raiser. This compiles.

const parsed = JSON.parse('[1]')
const bar = 1 + parsed

In fact, not only does it compile. Type coercion trumps annotations. You can see with with the string representation of 11 in the output below. The annotations feel closer to type hints than actual annotations.

const parsed: number = JSON.parse('[1]')
const bar: number = 1 + parsed
const baz: number = parsed[0] + 1
11 2

We’re effectively seeing the Javascript bleed through with the any and the type coercion. I’m shocked4 that for how popular this language is, and for how vocal the proselytizers are, the type-checker isn’t catching these things. Things that people say you should use Typescript for. This is supposed to be it’s core competency, turning runtime errors into compile time errors. 5

Interestingly enough, if we change our example a little bit.

const parsed: number = JSON.parse('{"k": 1}')
console.log(parsed + 1, parsed.k + 1)

We do receive compile time error, but it’s for a reference to the field k. Not for the incompatible addition operation parsed + 1


foo.ts:2:32 - error TS2339: Property 'k' does not exist on type 'number'.

2 console.log(parsed + 1, parsed.k + 1)
                                 ~


Found 1 error.

But if we modify it a bit more, we once again escape the compiler errors

const parsed: number = JSON.parse('{"k": 1}')
console.log(parsed + 1, parsed["k"] + 1)

I’m not saying typescript is all bad.6. Sometimes you’re stuck needing to write something in-browser, or the tool you need is written in javascript. But if one is reaching for typescript just for the type system there are certainly better options out there.

Examples

I’ve had it pointed out by some that have grown up in javascript and have Stockholm syndrome never been exposed to other languages and paradigms, that “Typescript transpiles to Javascript and Javascript doesn’t have types so there are limitations”. 7 Do you know what also doesn’t have types? Machine code. You know what a whole gang of statically typed languages use as a compilation target? Machine code.

Here are some examples. Not only do we get compilation errors, for some it feels awkward to write them incorrectly due to the type annotations and how the language generally operates.

OCaml

If we try to do the same operation in OCaml

let () =
  let open Yojson.Basic.Util in
  let foo = Yojson.Basic.from_string "{\"k\": 1}" in
  print_int (foo + 1)

We get a compile time error instead of a runtime error

4 |   print_int (foo + 1)
                 ^^^
Error: This expression has type Yojson.Basic.t
       but an expression was expected of type int

But if we explicitly unpack and cast..

let () =
  (* no error handling *)
  let open Yojson.Basic.Util in
  let foo = Yojson.Basic.from_string "{\"k\": 1}" in
  print_int (
    to_int(
      member "k" foo
    )+1
  );
  print_newline ();

we get

2

Haskell

Again, like the typescript example attempts.

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Main(main) where

import Prelude.Compat
import Data.Aeson
import Lib
import Control.Applicative (empty)

data Foo = Foo { k :: Integer }
        deriving (Show)

instance FromJSON Foo where
    parseJSON ( Object v )  = Foo <$>
                              v .: "k"
    parseJSON _             = empty


main :: IO ()
main = do
    let bar = decode "{\"k\":1}" :: Maybe Foo
    bar+1 {- Specifically this line -}

Unsurprisingly, a compile time error

    • Couldn't match type ‘Maybe’ with ‘IO’
      Expected: IO Foo
        Actual: Maybe Foo

but if we swap bar+1 out with some pattern matching

    case bar of
        Just(Foo(i)) -> print (i+1)

we get the expected output

2

java

Java’s a little different than the above. It’s similar to how you should actually handle this in typescript. You create an object, and instantiate an instance of your object with the json object.

import com.google.gson.Gson;

public class foo {
  private class Bar {
    public Integer k;
    public Bar(Integer i){
      this.k = i;
    }
  }
  public static void main(String[] args) {
    Gson json = new Gson();
    Bar bar = json.fromJson("{\"k\": 1}", Bar.class);
    System.out.println(bar+1);
  }
}

But that said, it still catches this, where typescript would not.

foo.java:13: error: bad operand types for binary operator '+'
    System.out.println(bar+1);
                          ^
  first type:  foo.Bar
  second type: int
1 error

  1. Expect a separate post on that later. ↩︎

  2. https://discuss.OCaml.org/t/multicore-OCaml-dec-2020-jan-2021/7225 ↩︎

  3. I’m still puzzled as to why the C preprocessor is evil while go templating will save us all. ↩︎

  4. Not really, popular != (good | sane) ↩︎

  5. technically we didn’t even get a runtime error. ↩︎

  6. I’m just saying that javascript and the entirety of it’s ecosystem is. ↩︎

  7. I kid you not. ↩︎