DEV Community

Cover image for TypeScript or F#?!? WHICH IS BETTER?!
Jakub Švehla
Jakub Švehla

Posted on • Updated on

TypeScript or F#?!? WHICH IS BETTER?!

Today I'll try to compare two different programming languages, F# and TypeScript.
Both of them are multiplatform, high-level, and statically-typed so we're able to use them to write any kind of type-safe application like web-servers, native-apps, web-applications or we can use them for daily scripting on your favourite operating system.

Comparison categories

I would like to focus on a few concepts of programming languages that we are going to compare. Those categories are:

  1. Data-type inferring
  2. Generic inferring
  3. Dynamic object types
  4. Null pointer exceptions
  5. Pattern matching
  6. Modules import system
  7. Runtime optimization
  8. Not owned by mega-corporate

TLDR: goto final conclusion

1. Data-type inferring

F# is a strongly-typed language that implements an algorithm called Hindley–Milner. That algorithm analyzes your codebase and infers as much static types as possible.

If we want to write a function to sum two numbers in TypeScript, we have to define the data types for both arguments, and the TypeScript compiler will infer the Return type

TS

// this is a function which takes 2 arguments: `a`, `b`, the function sums those numbers and returns a value
// here we have to define the data types of both arguments
let sum = (a: number, b: number) => a + b
Enter fullscreen mode Exit fullscreen mode

fs obj example

On the other hand, F# analyses the source code and finds that the + operator can be called on two integers so it infers the data types of the arguments.

F#

// this is a function which takes 2 arguments: `a`, `b`, the function sums those numbers and returns a value
let sum a b = a + b
Enter fullscreen mode Exit fullscreen mode

fs obj exapmle

F# is running on .net ecosystem so it makes a difference between float and int while TypeScript does not

As you can see, F# code looks like another scripting language without static types but because of the terrific F# compiler it's a type-safe and strongly typed snippet.

Thanks to the Hindley-Milner algorithm in the F# compiler, F# is the winner.
Unfortunatelly it looks like TypeScript has no plan to implement this kind of type inference.

🏆 F# won

2. Generic inferring

TypeScript use angle brackets <T> for generics. This is very similar to languages like C++, Java or C#. F# use just simple apostrophe 'T for it.

In TypeScript, we're able to infer simple return values and variable declarations, but if you want to have dynamic arguments like in this example:

TS

const toDoubleTuple = <T>(anything: T): [T, T] => [anything, anything]
Enter fullscreen mode Exit fullscreen mode

you have to define that the argument anything is of some generic type T and the function takes the type of anything and returns a tuple of [anything, anything]

As you may suggest, F# is so smart that it can even infer generic types of our function.

F#

let toDoubleTuple anything = anything, anything
Enter fullscreen mode Exit fullscreen mode

fs head

Again, F# analyzes the source code and finds that if a function takes anything of type 'a, it returns a tuple of anything, anything of type 'a * 'a. F# compiler infers that argument is of Generic type 'a. This code looks like a dynamic scripting language similar to Python but it has a powerful 100% type-safe runtime.

🏆 F# won

3. Dynamic object types

Now, let's compare how to create data-type and structural data objects. Let's start with F#.

F#

type User = {
  id: string
  name: string
}

let kuba = {
  id = "my-id-123"
  name = "Kuba"
}
Enter fullscreen mode Exit fullscreen mode

fs obj example

As you can see, F# has structurally based data object types. It means that F# compiler finds that there exists an object type with attributes id: string and name: string and it automatically infers its data type.

In TypeScript, if we create a data type, we have to assign that data type to a variable.

TS

type User = {
  id: string
  name: string
}
// assign the data type to the variable
let kuba: User = {
  id: 'my-id-123',
  name: 'Kuba'
}
Enter fullscreen mode Exit fullscreen mode

It's verbose so we have an alternative in TypeScript and we can infer the data type directly from the data by using the typeof keyword.

TS

let kuba = {
  id: 'my-id-123',
  name: 'Kuba'
}
type User = typeof kuba
Enter fullscreen mode Exit fullscreen mode

ts obj exapmle

Thanks to the TypeScript approach, we may use more advanced generics like Omit<...>, Pick<...>, keyof and so on.

F# structural-based data object types are awesome but here the TypeScript has much more powerful, nicer and minimalist tools to define dynamic data object types.

🏆 TypeScript won

4. Null pointer exceptions

In JavaScript we have a few nullable values: null, undefined, 0, NaN, "". This makes it difficult to handle the JavaScript runtime properly. Thanks to TypeScript we can check the types more strictly.

TS

let pipePrint = <T>(data: T | undefined | null) => {
  if (data === undefined || data === null || isNan(data)) {
    console.log('no value provided')
  } else {
    console.log(data)
  }
}
Enter fullscreen mode Exit fullscreen mode

F# decided not to implement those nullable values and focused on forcing you to handle edge-cases strictly. So in the F# core there is a defined union type called Option
It's defined as:

F#

type Option<'a> =       // use a generic definition
   | Some of 'a           // valid value
   | None                 // missing value
Enter fullscreen mode Exit fullscreen mode

If we wrap some value in that Option type, we're able to check if the value exists or if the value is empty.

F#

let pipePrint data = 
   match data with
     | Some x -> printf x
     | None -> printf "no value provided"
Enter fullscreen mode Exit fullscreen mode

Thanks to that, F# compiler forces us to handle all non-valid potential null pointer exception errors, which is good. But in TypeScript, we have the same result if we type 100% of our codebase correctly. So in this category it is a draw and there is no winner.

🏆 no winner

5. Pattern matching

Pattern matching is a really powerful part of F# language design. There are many sources where you can find more information, like there or there.

TLDR:
Pattern matching in JavaScript/TypeScript is bad, not flexible, and bad again. So there the F# is the winner.

I put here one of many examples of the powerfulness of pattern-matching in F#.

F#

let vectorLength vec =
    match vec with
    | [| var1 |] -> var1
    | [| var1; var2 |] -> sqrt (var1 * var1 + var2 * var2)
    | [| var1; var2; var3 |] -> sqrt (var1 * var1 + var2 * var2 + var3 * var3)
    | _ -> failwith (sprintf "vectorLength called with an unsupported array size of %d." (vec.Length))
Enter fullscreen mode Exit fullscreen mode

🏆 F# won

Modules import system

F# has linear module system with namespaces and modules.

TypeScript has a few module systems like commonjs, ES6, and so on. Today we're gonna talk about ES6 imports.

To be honest, I don't like namespaces in programming languages like C#, Java, PHP, and so on. I prefer a module system where each file is its namespace and import & export are the only sign if the value is local, or if the value can be imported into a different module.

F#

// file-a.fs
module FileA
let sum a b = a + b

// file-b.ts
module FileB
open FileA

Enter fullscreen mode Exit fullscreen mode

ES6 modules enable us to have cyclic dependencies where names of files are part of our application architecture. In a language like C# and F# we have to create a file plus add a module or namespace declaration. I prefer a more minimalistic way of defining modules so I prefer the ES6 way.

TS

// file-a.ts
export const sum = (a, b) => a + b


// file-b.ts
import { sum } from './file-a.ts'
Enter fullscreen mode Exit fullscreen mode

🏆 TypeScript won

7. Runtime optimization

Both languages have really strong type inference systems. This means that the compiler will check the code and suggest (infer) the best static type to use. Thanks to that, you are able not to define static-type in the code on your own. It helps you to make a better development experience with less writing, but more readable, self-documented, and less error-prone code.

TypeScript is a highly dynamic language so if we compile our TypeScript code into the vanilla JavaScript, it removes static types and we're not able to use those metadata to optimize JavaScript runtime like memory allocation and CPU time.

On the other hand, thanks to the fact that TypeScript is a tool for a better developer experience, we're able to use expressions like as any, : any, @ts-expect-error, @ts-ignore and not to be too strict.

It means that both approaches have pros and cons, so there is no winner.

🏆 no winner

8. Not owned by mega-corporate

As you may know, F# is developed by Microsoft and it runs on the .net platform created by Microsoft as well. TypeScript is also created by Microsoft but the final output is pure vanilla JavaScript which is not owned by any large mega-corporate. Thanks to that, it gave us (as developers) the option not to be locked in one ecosystem of some mega-corporate and we are able to feel more free and independent.

Maybe this point could be stupid for you but I believe that it's better to write code in a language that is not so directly connected to anyone and there is more democracy.

🏆 TypeScript won

Final conclusion

So, Let's check the results of categories:

F# TS
1. Data-type inferring 🏆
2. Generic inferring 🏆
3. Dynamic object types 🏆
4. Null pointer exceptions -- --
5. Pattern matching 🏆
6. Modules import system 🏆
7. Runtime optimization -- --
8. Not owned by mega-corporate 🏆

So as you you can see, it's hard to decide which language I like more.
In conclusion, my dream language will be F# with Javascript ES6 module system and object data inferring via typeof.

Is there a solution?

So at the end of this article, it is worth mentioning that there is an awesome compiler fable.io that brings F# to the JavaScript ecosystem.

On the next screenshot you can see demo example where fable.io transpile one of our example from F# into the JavaScript.

fable example

If you enjoyed reading the article don’t forget to like it to make my day

Discussion (2)

Collapse
szymonszym profile image
szymon-szym

Hi Jakub, nice post! I am in love with both TS and F#.
I would argue that in the nulls category F# is a clear winner. Not having nulls in the language is huge, and I believe that the whole point is to force developers to deal with None cases explicitly. In F# you can chain Options in the nice pipelines. In TS you would need to use library, e.g. fp-ts (which is amazing btw).
In my opinion, the huge advantage of TS is the ecosystem. Nowadays it is hard to find popular npm packages without types declaration (a big part of them is written in TS in the first place). Using those packages from TS code is seamless. On the other hand, most libraries in the .NET ecosystem were created in and for C#. Utilizing them in the F# feels rusty. You need to deal with nulls, mutable classes, and other "non-fsharpy" stuff.
As I said I love both languages. My default is F#, but I am pleased to go with TS if it makes more sense.

Collapse
svehla profile image
Jakub Švehla Author

Hello 👋 thanks for review!

I 100% understand your arguments and i feel it! I'm still not a sure if prefer one or the other approach. In my life a wrote a lot of TypeScript code and not all of it was super super-production-ready app which has to be without errors. So i belive that there is some ratio between productivity and safety. Because of that i really like that you're able to write some sh**y code where only success path is working and if you want you may do it type-safe. But if you want to do some POC as fast as possible you just put @ts-expect-error here and you're fine :D

I'll think about it more..