Typescript and TSX

This is an introduction to Typescript and TSX from the point of view of someone that is familiar with a C based language.

Typescript

Typescript is a superset of Javascript that provides a combination of static typing and features from the latest ECMA standards.

Classic vs Modern Javascript

You likely have some preconcieved notions of Javascript. Many of these notions are likely inaccurate today as most people's experience with Javascript is limited to ES5, a standard published in 2009. Those were the dark times of the internet.

Don't worry, you won't have to touch JQuery within an Electric UI project.

You may have interacted with code that looks like this:

// Rectangle 'class'
function Rectangle(x, y, w, h) {
this.x = x
this.y = y
this.w = w
this.h = h
}
 
Rectangle.prototype.move = function(x, y) {
this.x += x
this.y += y
}
 
Rectangle.prototype.printPos = function() {
console.log('Our position is (' + this.x + ', ' + this.y + ')')
}

This is 'old Javascript'. In the more modern, idiomatic, Typescript:

class Rectangle {
x: number
y: number
w: number
h: number
 
constructor(x: number, y: number, w: number, h: number) {
this.x = x
this.y = y
this.w = w
this.h = h
}
 
move(x: number, y: number) {
this.x += x
this.y += y
}
 
printPos() {
console.log(`Our position is (${this.x}, ${this.y})`)
}
}

Much more sane.

Some quick facts:

  • Modern Javascript is amongst the fastest interpreted languages. (It's faster than Python)[^1]
  • It has a huge community backing it. [^2]
  • Javascript is the most popular language in the world. [^3]

Of course, it is still an interpreted language. It is slower than C or Rust. Don't do heavy number crunching in Javascript. Do write user interfaces in it.

[^1]: Based on "The Computer Language Benchmarks Game" https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/python.html [^2]: Based on how many GitHub repositories there are. https://github.com/topics/javascript?l=javascript [^3]: Based on Stack Overflow's 2018 survey available here: https://insights.stackoverflow.com/survey/2018/

Basics

Variables

Variables can be defined with either const or let , defining their mutability.

const immutableVariable = 43
let mutableVariable = 'hello'

In older Javascript codebases, you'll often see the var mutableVariable = "hello" style of definition.

It is not recommended to use var as it pulls the definition out of the local scope.

Functions

function addNumbers(a: number, b: number) {
return a + b
}

The return type is inferred automatically.

Arrow functions

Arrow functions provide a concise syntax that allow for implicit returning of the singular expression.

const returnFortyTwo = () => 42

Typing

Typescript adds static typing on top of modern ECMA features. It is often used to define the allowable types for a component.

interface PersonProps {
name: string
age: number
}

Using static types allows for better IDE hinting and realtime error checking, and makes runtime errors obvious as soon as a mistake is made, rather than obscuring the bug behind runtime checks and fixes.

This website even makes use of this information, hovering over types, functions and expressions in codesnippets will show the type or function signature inline!

TSX

TSX is a declarative, XML like syntax that layers on top of TypeScript to provide a concise, expressive method for defining the structure of a user interface.

What it is by example

Our component, Greeting, takes a name prop and renders a simple div with the greeting css class, greeting the named party.

// An interface defines the allowable props for the component
interface GreetingProps {
name: string
}
 
// The Greeting function is our component, taking the props and returning more components.
const Greeting = (props: GreetingProps) => {
const { name } = props
 
return <div className="greeting">Hello, {name}</div>
}

To use the component, we simply write the following:

<Greeting name="Tim" />

How does it work?

In order to immediately remove the magic, we'll quickly walk through how this becomes 'real code'.

TSX tags are simply transpiled into React.createElement calls.

function createElement(
type, // either another React component, or a primitive HTML element tag name
props?, // an object of attributes and their values
...children // a variadic argument of children of this element
)

Our component:

<Greeting name="Tim" />

becomes a simple function call:

React.createElement(Greeting, { name: 'Tim' }, null)

Our component function similarly becomes:

const Greeting = (props: GreetingProps) => {
const { name } = props
 
return React.createElement('div', { className: 'greeting' }, 'Hello, ', name)
}

When React renders our component, it collapses this tree heirarchy of components down to function calls that render the following html:

<div class="greeting">Hello, Tim</div>

It's quite terse to write out React.createElement calls everywhere, TSX elements are simply syntaxial sugar.

Why not a templating language layered on HTML?

Templating languages inevitably require 'more' from the developer. A for loop becomes a for loop with an index counter and a modulus operation. A unix datetime needs to be formatted to be human readable. Then it needs to be formatted in the same locale as the human. A string needs to be slugified in order to build a URL.

Code must then be written out of the scope of the template language that passes 'more processed' variables back into the templating language. The same UI concern is then moved across multiple files.

Templating languages are often imperative in nature, describing step by step how to render the html. Mistakes will often result in unclosed tags and garbage markup.

These practical shortcomings are combined with an architectural one. User interfaces tend to be able to be generalised as a series of nested rectangles. The description of these interfaces lends itself to a declarative language rather than an imperative one.

As TSX is not a templating language but syntaxial sugar on top of function calls, these problems evaporate.

If a component is syntaxially correct locally, you can be sure of global safety regarding markup, unclosed tags are never a problem.

Tight integration with Typescript

TSX can be 'broken out of' with brackets.

<div>Seven can be written as: {2 + 5}</div>

Which will be compiled to:

React.createElement('div', null, 'Seven can be written as: ', 2 + 5)

Which renders as:

<div>Seven can be written as: 7</div>

This is how we injected the name into our greeting in the first example.

If we wanted to do more complex processing, we can do anything!

interface GreetingProps {
name: string
}
const GreetingWithBranches = (props: GreetingProps) => {
const { name } = this.props
// If name is Tim, we give a different greeting
if (name === 'Tim') {
return <div className="greeting">Oh hey {name}!</div>
}
// If their name is 'long', we tell them that as well.
if (name.length > 7) {
return (
<div className="greeting">
Hello, {name}. Your name has over {name.length - 1} characters!
</div>
)
}
return <div className="greeting">Hello, {name}</div>
}

We can even do some computer science.

const rainbow = [
'#9400D3',
'#4B0082',
'#0000FF',
'#00FF00',
'#FFFF00',
'#FF7F00',
'#FF0000',
]
interface GreetingProps {
name: string
}
const GreetingRainbow = (props: GreetingProps) => {
const { name } = this.props
// turn our name into an array of letters
const arrayOfLetters = Array.from(name)
// map our array of letters to an array of spans with an inline style that sets their color
const spans = arrayOfLetters.map((letter, index) => (
<span style={{ color: rainbow[index % rainbow.length] }} key={index}>
{letter}
</span>
))
return <div className="greeting">Hello, {spans}</div>
}

Conventions

Components are pure functions

Components must be pure functions with respect to their props. They must not mutate their props.

This is an example of an impure function, since it modifies its props.

interface NotAllowedProps {
total: number
price: number
}
 
const NotAllowed = (props: NotAllowedProps) => {
props.total -= props.price
 
return <div>Total: {props.total}</div>
}

Component case

All React components must be Titlecase. All lowercase names are reserved for the primitive html elements.

This is not allowed:

<configurator />
Property 'configurator' does not exist on type 'JSX.IntrinsicElements'.2339Property 'configurator' does not exist on type 'JSX.IntrinsicElements'.

Composition vs Inheritance

Composition of components is strongly recommended over class based inheritance. This guide has not gone through class based components as a result. In the vast majority of cases, functional components like those described here are sufficient.

The children prop

A special prop called children is delivered to components with child elements:

interface ComponentWithChildrenProps {
children: React.ReactNode
}
 
const ComponentWithChildren = (props: ComponentWithChildrenProps) => {
return <div style={{ border: '1px solid red' }}>{props.children}</div>
}
 
const PageComponent = () => {
return (
<ComponentWithChildren>
This text will have a red border.
</ComponentWithChildren>
)
}

Rendering PageComponent will produce text with a red border.

Control flow within a component

As the contents of TSX elements become expressions within a function call, imperative control flow is not allowed.

interface IncorrectProps {
branch: boolean
}
 
const Incorrect = (props: IncorrectProps) => {
return <div>
{if (props.branch) {
Expression expected.1109Expression expected.
return <span>Yes</span>
Expression expected.1109Expression expected.
} else {
Unexpected token. Did you mean `{'}'}` or `}`?1381Unexpected token. Did you mean `{'}'}` or `}`?
return <span>No</span>
Expression expected.1109Expression expected.
}}
Unexpected token. Did you mean `{'}'}` or `}`?
Unexpected token. Did you mean `{'}'}` or `}`?
1381
1381
Unexpected token. Did you mean `{'}'}` or `}`?
Unexpected token. Did you mean `{'}'}` or `}`?
</div>
}

The above becomes:

interface IncorrectProps {
branch: boolean
}
 
const Incorrect = (props: IncorrectProps) => {
return React.createElement("div", {}, if (props.branch) {
Argument expression expected.1135Argument expression expected.
return React.createElement("div", {}, "Yes")
} else {
return React.createElement("div", {}, "No")
})
Declaration or statement expected.1128Declaration or statement expected.
}

Which is not correct, an expression cannot contain an if statement.

There are two correct ways to achieve this goal, either imperative control flow outside of the TSX elements, or a ternary expression within the TSX elements.

Imperative control flow outside the expression
interface ImperativeControlFlowProps {
branch: boolean
}
 
const ImperativeControlFlow = (props: ImperativeControlFlowProps) => {
if (props.branch) {
return <span>Yes</span>
} else {
return <span>No</span>
}
}

The above becomes:

interface ImperativeControlFlowProps {
branch: boolean
}
 
const ImperativeControlFlow = (props: ImperativeControlFlowProps) => {
if (props.branch) {
return React.createElement('div', {}, 'Yes')
} else {
return React.createElement('div', {}, 'No')
}
}
Ternary Expression
interface TernaryExpressionProps {
branch: boolean
}
const TernaryExpression = (props: TernaryExpressionProps) => {
return {props.branch ? <span>Yes</span> : <span>No</span>}
}

The above becomes:

interface TernaryExpressionProps {
branch: boolean
}
const TernaryExpression = (props: TernaryExpressionProps) => {
return {
props.branch
? React.createElement("div", {}, "Yes")
: React.createElement("div", {}, "No")
}
}