How TypeScript can unite the Software Engineering tribes

Mike Sharp
November 15, 2019
8 mins
Engineering

"TypeScript is a typed superset of JavaScript that compiles to plain JavaScript" - typescriptlang.org

It’s really as simple as that: it’s a programming language with all of the features of JavaScript, but adds some exciting additions that will make your development team more cohesive and productive.

TypeScript introduces a comprehensive typing system on top of JavaScript. Strong typing makes it simple to introduce lots of new syntactical additions to the JavaScript developers toolbox, making it easier to explicitly implement and communicate software design and architectural patterns.

It may be a lot of syntactic sugar on top of JavaScript, but it’s still pretty sweet. Let’s explore further.

How explicit modelling benefits the development process

Explicit modelling makes it simpler for developers to maintain and extend the codebase because the intent of the code is made clearer by the extended vocabulary provided by TypeScript.

Using TypeScript simplifies the process of writing code that adheres to SOLID principles because the semantics commonly associated with those principles, such as abstract interfaces and classes, are codified in the language. There is no need to infer these constructs because they are first class features in TypeScript.

How we use TypeScript at Amplience

Our team started experimenting with TypeScript during some self-contained integration projects. These projects consisted of an API-controller layer, backed up by services to do the heavy lifting and to communicate with external and internal APIs.

Team composition

We had a particularly eclectic team composition for the project consisting of:

  • Back-end developers who were familiar with strongly typed languages

  • QA developers used to writing tests in Ruby

  • Front-end developers with grounding in modern ES6 JavaScript features.

The team was diverse in terms of experience and job role, but TypeScript offered something familiar for everyone, including:

  • Strong typing, classes, interfaces familiar to back-ends

  • A familiar JavaScript syntax for front-ends

  • A strong set of tools available to QA for integration testing

All members of the team had to overcome their own personal learning curves to become familiar with TypeScript and its language features, but working with a common language and tools made a huge difference to the way the team worked together.

How our workflow was enhanced

By leveraging the same models, interfaces and fixtures between QA and development, the team were able to reason better about how the projects were built. All of this was made possible because TypeScript gives you the language features that make it simpler to write high-quality and maintainable code.

The TypeScript compiler robustly enforces the rules we put in place, so there was less ambiguity when multiple developers and disciplines worked on the codebase.

Challenges we would have faced if we hadn’t used TypeScript

The patterns we used when developing the project may have been implemented implicitly if we had used JavaScript instead, but this would have led to a set of problems that TypeScript manages to avoid.

We could have designed the classes and models on paper and agreed to stick to them, then used plain JavaScript objects to represent our models.

We definitely could have taken the pencil and paper approach, but surely there’s a better way?

The answer is a resounding ‘Yes’, and the solution is to use TypeScript.

TypeScript’s strong typing makes it simpler to explicity implement design patterns and OO concepts, such as the factory function pattern demonstrated below.

1export interface Cake {
2  eat(): void;
3}
4
5enum CakeType {
6  CHOCOLATE_CAKE = 'chocolate',
7  CARROT_CAKE = 'carrot',
8  FAKE_CAKE = 'fake'
9};
10
11class ChocolateCake implements Cake {
12  public eat(): void {
13    console.log('yum, this chocolate cake sure is tasty...');
14  }
15}
16
17class CarrotCake implements Cake {
18  public eat(): void {
19    console.log('oh, this is a carrot cake, wow');
20  }
21}
22
23class FakeCake implements Cake {
24  public eat(): void {
25    console.log('the cake is a lie...');
26  }
27}
28
29function CakeFactory(cakeType: CakeType): Cake {
30  switch (cakeType) {
31    case CakeType.CHOCOLATE_CAKE: {
32      return new ChocolateCake();
33    }
34    case CakeType.CARROT_CAKE: {
35      return new CarrotCake();
36    }
37    case CakeType.FAKE_CAKE: {
38      return new FakeCake();
39    }
40    default: break;
41  };
42}
43
44function tryCake(): void {
45  const cakeA: Cake = CakeFactory(CakeType.CARROT_CAKE);
46  const cakeB: Cake = CakeFactory(CakeType.FAKE_CAKE);
47  const cakeC: Cake = CakeFactory(CakeType.CHOCOLATE_CAKE);
48
49  cakeA.eat();
50  // Output: 'oh, this is a carrot cake, wow'
51
52  cakeB.eat();
53  // Output:  'the cake is a lie...'
54
55  cakeC.eat();
56  // Output:  'yum, this chocolate cake sure is tasty...'
57}
58
59tryCake();

Had we chosen not to use TypeScript, the team would have been deprived of the ability to check types at compile-time and in our text editors using a linter. We would have been throwing away an array of tools that do the heavy lifting and ensure we’re making the system work as intended.

Data inconsistency, re-definition and duplication would have always been a risk, and the Object-oriented constructs we benefited from in TypeScript would not have been formalised had we relied on implicitly implemented patterns.

As an example, the following JavaScript code is written defensively because we can’t be sure that the params have been implemented.

1function processRequest(params) {
2  // does this look familiar?
3  if (params.query){ 
4    doSomethingWithQueryParam(params.query);
5  }
6  if (params.body){ 
7    doSomethingWithBodyParam(params.body);
8  }
9  if (params.someOptionalParamADeveloperAddedToPerformOneExtraTask) {
10    doThatOneExtraTask(params.body);
11  }
12  // code is written defensively because we can never be certain these params have been implemented
13  // wouldn't it be better if the params were properly formalised for all to see and type-checked by the compiler?
14}
In TypeScript, an interface enforces the consistency of the model. Optional parameters are clearly labelled and required fields are enforced at compile-time.
1interface RequestParams {
2  query: String;
3  body?: RequestBody
4};
5
6function processRequest(params: RequestParams) {
7  // we can call this with confidence the required param will always exist
8  doSomethingWithQueryParam(params.query);
9  // we still have to check if body exists
10  // but it will be clear this is intentional with an interface
11  if (params.body) {
12    doSomethingWithBodyParam(params.body);
13  }
14}

These patterns might have been implemented using plain old JavaScript, but without a type system or OOP-focused syntax there would have been no way to enforce this. The burden would have been on developers to clearly express the constructs through code writing style, writing robust tests (which should still be done of course), or introducing comments into the code. In our experience you lose a lot of context in these comments as the system evolves over time.

All in all we found TypeScript to be a great language and tool to use in delivering our project, and each of us learned a little something along the way.

The problem with a disjointed stack

There can be a disconnect between front-end and back-end developers working on different parts of the same application, perhaps working in different programming languages and with different technologies. This lack of awareness can be problematic when new features are being developed that require input from both sides of the stack in order to properly design a solution.

The disconnect is illustrated in the image below, which shows that the natural meeting point between the two disciplines is the API layer.

The possible solution

This chasm can be filled by adopting a common dialogue within a project. The dialogue could take the form of understanding a common set of design and architectural patterns at the high level, or if we were to go a little lower level, we could even use the same programming language within the stack so that components can be more easily described and communicated amongst all team disciplines.

How using TypeScript changes things for the better

We believe that TypeScript is the language to unite and align the entire stack and has the potential to make teams more cross-functional or ‘T-shaped’.

Developers learning TypeScript who have a JavaScript background, or those more familiar with Java or C#, face a similar learning curve. We believe that both sides will find a point of convergence, provided that all members of the team ‘buy in’ and teach each other about their area of expertise.

Developers used to the flexibility of JavaScript will take some time to love TypeScript’s strong typing. Your team may benefit from some pair programming to share knowledge. Even if none of the developers on the team are familiar with strongly typed languages, it’s still a valuable task to do to get the whole of the team talking on like-minded terms and learning together.

1interface IA {
2  doThing(): void;
3}
4
5enum ThingTypes {
6  THING_ONE = 1,
7  THING_TWO = 2
8}
9
10class A implements IA {
11  public doThing() {
12    console.log('calling doThing method');
13  }
14}
15
16class B extends A {
17  public doExtendedThing() {
18    console.log('Thing from extended class');
19  }
20  public doEnumThing(thing: ThingTypes) {
21    if (thing === ThingTypes.THING_ONE) {
22      console.log('this is thing one');
23    } else {
24      console.log('this is thing two');
25    }
26  }
27}
28
29const objA = new B();
30
31objA.doThing();
32// output: 'calling doThing method'
33objA.doExtendedThing();
34// output: 'Thing from extended class'
35objA.doEnumThing(ThingTypes.THING_ONE);
36// output: 'this is thing one'
37objA.doEnumThing(ThingTypes.THING_TWO);
38// output: 'this is thing two'

The back-end developers will need to learn to adapt to JavaScript’s dynamic nature and some of its unique quirks and gotchas, but this can be mitigated by working alongside colleagues more familiar with the idiosyncrasies of the language.

Here are a few of the JavaScript language rules that can be confusing to newcomers:

1const thing = {
2  a: 'a'
3};
4
5const thing2 = {
6  b: 'b'
7};
8
9// Developers new to JavaScript may not be aware of how Array and Object
10// utility functions work, they will need to become familiar with what 
11// JavaScript includes out of the box and what it doesn’t
12const combinedObject = Object.assign(thing, thing2);
13const someArray = [‘a’, ‘b’, ‘c’];
14const someStringFromArray = someArray.split(‘’).join(‘’);
15
16// Javascript has null and undefined
17let uninitialised; 
18console.log(uninitialised === null);
19// output: false
20console.log(uninitialised === undefined);
21// output: true
22
23// variables 'technically' can be declared after they are assigned
24function logHoisted () {
25  hoisted = 'hello';
26  console.log(hoisted);
27  let hoisted;
28}
29
30// JavaScript has some interesting types too
31const char = Number('h');
32console.log(char);
33// output NaN

Some killer JavaScript features of the future...available today in TypeScript

One perk of using TypeScript is that out of box you get access to some really cutting-edge JavaScript features that aren’t available natively in many browsers. This is the nature of using a compiled language.

While you can also use a tool such as Babel to get access to modern JavaScript features, such as those in the ES6 and ES7+ specifications, you then lose all of the type-checking benefits of using TypeScript.

You can even dive deep into the future of TypeScript

TypeScript is in active development and has a very good release cadence. You can even download beta releases of the language to gain access to new language features in advance- and let’s face it, features that are years away from native browser implementations in JavaScript.

Here’s some new functionality from the 3.7 beta of TypeScript that demonstrates the new optional chaining operator (look out for the ‘?’ operator in the code example below):

1// optional chaining (in TypeScript 3.7 beta)
2const dataStructure = {
3  people: [
4    {
5      name: 'Mike',
6      hobbies: ['programming', 'games'],
7      country: 'UK'
8    },
9    {
10      name: 'John',
11      occupation: 'Tractor maintainer',
12      country: 'UK'
13    }
14  ]
15};
16
17// hobbies field exists, output should be 'programming, games'
18const mikesHobbies = dataStructure.people[0].hobbies?.join(', ');
19// if the persons hobbies are undefined, the code shouldn't throw an exception but yield value 'undefined'
20const johnsHobbies = dataStructure.people[1].hobbies?.join(', ');
21
22// POJO's vs typed objects
23console.log('mikes hobbies: ' + mikesHobbies);
24console.log('johns hobbies: ' + johnsHobbies);

Closing thoughts

TypeScript has been a good fit for our projects at Amplience, and it doesn’t look like it’s going away any time soon. TypeScript is growing in popularity, has robust documentation and an enthusiastic community behind it.

We think that using TypeScript will make your development teams happier, more T-shaped and better communicators- and it will also help them write more resilient and maintainable code.

As the old saying goes, the proof of the pudding is in the eating. So dig in and see how TypeScript can change the way your teams work for the better.