Strict: A Language for Strict Functional Protocols

Strict: A Language for Strict Functional Protocols

TypeScript is great. It really is. It converts the old JavaScript code we all know to a modern language with proper classes, interfaces, type checks, and many more features. I literally can’t think about going back to JavaScript. Never.

Yet, TypeScript has its own limitations by being a strict syntactical superset of JavaScript. Meaning it must inherit all the bad parts of JavaScript and it can’t go any further without serious support from the ECMAScript Standard.

For me, the most annoying thing using TypeScript is that I still can’t define proper function interfaces and make sure the programmer will know exactly how to use them without some documentation. Indeed, this problem is not unique only for TypeScript, but it feels so close it’s frustrating.


1. Error Handling

The most fundamental issue is that TypeScript does not support throwing decelerations and as far as I know, there is no plan to add it anytime soon.

There is no programmatic way I can indicate the programmer that my function may throw if the input is not “a string with an odd number of characters”:

declare function getMiddleChar(input: string): string
  throws EvenStringError | LongStringError;

Nevertheless, even if it does, TypeScript can’t offer us a way to distinguish between different types, so we must use inheritance with an instanceof check:

class EvenStringError extend TypeError {};
class LongStringError extend TypeError {};

if (err instanceof EvenStringError) {
  // ...
} else if (err instanceof LongStringError) {
  // ...
}

2. Data Validations

Another feature that I really want to have is static data validation.

Imagine you are implementing a third-party service, with complicated validation for each endpoint. Wouldn’t it be better for every programmer who uses your library to get a compile-time error if he uses your functions wrong?

import { validate as isValidEmail } from "email-validator";

type EmailString(email: string) {
  if (!isValidEmail(email)) {
    throw new TypeError('Email is not valid');
  }
}

declare function sendTestEmail(email: EmailString):
  Promise<void, UnreachableServerError>;

// Compile Error: Email is not valid
await sendTestEmail("foo@examplecom");

The value check can only happen once when we are converting a string to a ValidEmail type, and we can use the type validation for runtime checks as well:

const strEmail: string = 'foo@example.com';
const email: EmailString = 'foo@example.com'; // compile time check

const runTimeEmail: EmailString = Email(strEmail); // run time check

// we can also use automatic runtime conversions
const email3: EmailString = strEmail;

We can even have type inheritance automatically:

type GMailEmail(email: Email) {
  if (!/@gmail\.com$/.test(email)) {
    throw new TypeError('Email is not under gmail.com');
  }
}

// Compile Error: Email is not under gmail.com
const email: GMailEmail = 'foo@example.com';

// Compile Error: Email is not valid
const email2: GMailEmail = 'foo@examplecom';

const foo: GMailEmail = 'foo@gmail.com'; // runs validations
const someEmail: Email = foo; // no validations required

3. Runtime Validation

I found myself writing a large chunks of interface protocols for my functions. Those protocols are great for compile time checks, but in many cases I also need to write specific validations for the user inputs according to those interfaces:

interface UserData {
  firstName: string;
  lastName: string;
  email: string;
}

const UserSchema = Joi.object({
  firstName: Joi.string().alphanum().min(2).max(30).required(),
  lastName: Joi.string().alphanum().min(2).max(30).required(),
  email: Joi.string().email().required(),
});

Those “code duplications” are really annoying. Using the Data Validators and existing interfaces, validation becomes a built-in feature:

interface UserData {
  firstName: NameString;
  lastName: NameString;
  email: Email;
}

try {
  // Run time validation
  UserData({ ... });
} catch (e) {
  // ...
}

4. Type Switch

Last but not least, supporting type switch to quickly detect the type of the return value could be really useful. Behind the scenes, naive implementation is just to call each type validation runtime function and test it. Future compile optimizations can improve performance by testing only specific properties:

const res: User | Business | string = await getDetails();

type switch(res) {
  case string:
    // ...
    break;

  case User:
    // ...
    break;

  case Business:
  default:
    // ...
    break;
}

What's next?

I’m not sure what’s the best way to make those features available. Forking the TypeScript project is a good option, but maybe a clean slate with backward compatibly to require TypeScript files can reveal many more features I can’t even think of (immutability is a thing!).

I would like to get some feedback and hear what do you think. Most importantly, I would like to know if you NEED those features as strongly as I do.

Thanks for reading!

Enjoyed the read?

Don’t miss out on my next article! to stay updated on the latest insights.

Need advice on your startup?

I’m available for interesting web projects. Send me some details and let’s start working!

Hire me
avataravataravataravataravatar

Let’s stay in touch!

Join 50+ other developers who receive my thoughts, ideas, and favorite links.