DEV Community

Discussion on: It's 2021 and I still don't know the best way to write this in TypeScript.

Collapse
 
peerreynders profile image
peerreynders • Edited

but we get the repetition of having to define the types in the class now. 🙄

I suspect your conundrum is more related to your perspective which perhaps has fallen victim to Software Development by Slogan.

type Person = {
  initial: string;
  length: number;
  initialRegex: RegExp;
  initialOccurance: number;
};

const person: (n: string) => Person = (name) => {
  const initial = name.charAt(0);
  const initialRegex = new RegExp(initial, 'g');
  return {
    initial,
    length: name.length,
    initialRegex,
    initialOccurance: (name.match(initialRegex) || []).length,
  };
};
Enter fullscreen mode Exit fullscreen mode

The DRY principle is often quoted in the context of elimination of repetition/duplication because of its expansion "Don't Repeat Yourself" - but that is just the slogan. The actual core message is:

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.

  1. Person - the type definition is authoritative representation of the type's shape - it exists in type space.
  2. person - the function is the authoritative representation of the type's construction - it exists in value space ‡.

So terms of DRY there isn't any duplication here because we are dealing with two separate dimensions of the same thing.

And in terms of class-based object-orientation I have a number of times run into the argument that class definitions that include constructors violate "Separation of Concerns" because they conflate the run-time behaviour of the object as defined by the methods with the construction of the object (largely because inept error handling inside a constructor can easily lead to "zombie objects" ruining your day).

I'm the last person to be a TypeScript apologist - OCaml's (i.e. ReScript) type inference is far superior without sacrificing soundness but gradual typing of JavaScript was a more important objective for the TypeScript team (for better or worse). Also at some point type inference becomes expensive in terms of CPU cycles (i.e. compilation times). While OCaml can be pretty swift, something like compiling Rust (with the additional borrow checking) can be rather time consuming. So being explicit with your types could perhaps - sometimes - improve compilation times.

JavaScript code lives entirely in value space, TypeScript types live in type space. TypeScript has some unique tooling to pull information from value space into type space - you mentioned the utility type ReturnType (es5.d.ts) which leverages type inference in conditional types.

Given the nature of JavaScript, TypeScript will often require explicit guidance and direction as to what information in value space is pertinent and how it is pertinent - and that guidance may look like repetition - but it's not.

Then there are design considerations. When learning Haskell you will often run into advice like this:

It's usually helpful to write down the type of new functions first;

Some people refer to this as type-driven development - I sometimes call it contract-first programming (leaning heavily on (positive) logic-to-contract coupling). You define your types and their relationships to one another before you implement any behaviour.
What you seem to be after is "Contract-last" development - i.e. have TypeScript divine from the implementation what the types should be.

In the Web Services space "Contract-last" was popular with developers because it made development easy - but in the majority of cases it lead to interoperability problems with clients using different host technologies. So "Contract-first" was the correct approach in most cases. Similarly designing your types first you often uncover constraints before you write a single line of code.

Would you ask a general contractor to build the house first and then derive the blueprint from the finished house?


‡ It would actually be more accurate to say that the function's type (n: string) => Person exists in type space while the function's implementation:

const person = (name) => {
  const initial = name.charAt(0);
  const initialRegex = new RegExp(initial, 'g');
  return {
    initial,
    length: name.length,
    initialRegex,
    initialOccurance: (name.match(initialRegex) || []).length,
  };
};
Enter fullscreen mode Exit fullscreen mode

exists in value space.

Collapse
 
reggi profile image
Thomas Reggi

This is a really great, in-depth explanation. I love it. Thank you for taking the time to write this out.