How I learned to stop worrying and love strong typings
Gian Marco Toso
@gianmarcotoso
gianmarcotoso
gianmarcotoso.com
polarityb.it
Software Engineer, The Pirate I Was Meant To Be
Born and raised in Turin, Italy
@gianmarcotoso
gianmarcotoso
gianmarcotoso.com
polarityb.it
MSc in Computer Engineering
Self Employed Software Engineer
Javascript and PHP Developer
Docker
PHP/Laravel
"It was working on my machine!"
The monolith is here to stay
NodeJS/TypeScript
Seriously, it's awesome!
React + Redux
The Flux Capacitor!
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript
- Official TypeScript Definition
class Person {
constructor(name) {
this.name = name
this.steps = 0
}
walk(steps) {
this.steps += steps
}
getSteps() {
return this.steps
}
}
interface CanWalk {
walk(steps: number): void
getSteps(): number
}
class Person implements CanWalk {
public name: string
protected steps: number
constructor(name) {
this.name = name
this.steps = 0
}
walk(steps: number): void {
this.steps += steps
}
getSteps(): number {
return this.steps
}
}
interface JSONSerializable {
toJSON(): object
}
class SomeComplexObject implements JSONSerializable {
/* ... */
}
function log(
message: string,
payload: JSONSerializable
) { /* ... */ }
More readable, better structured code
Errors stand out immediately
VSCode Integration + Intellisense
Consider a simple React Component
import React from 'react'
import * as PropTypes from 'prop-types'
class Image extends React.Component {
static propTypes = {
source: PropTypes.string.isRequired,
size: PropTypes.arrayOf(PropTypes.number).isRequired
}
render() {
const { source, size } = this.props
const [ width, height ] = size
const style = { width, height }
return (
<img
style={style}
src={source}
/>
)
}
}
export default Image
First issue: propTypes
static propTypes = {
source: PropTypes.string.isRequired,
size: PropTypes.arrayOf(PropTypes.number).isRequired
}
Checked at runtime (only in dev mode)
If the type is wrong, we'll see it at runtime within the browser's console
Wouldn't it be better if we knew right away?
Second issue: code completion
render() {
const { source, size } = this.props
const [ width, height ] = size
const style = { width, height }
// ...
}
Our editor has no way of knowing how the `props` object or the `size` is made, and cannot help us.
The `style` object could have any member, the editor doesn't know that we want it to be passed to a components' `style` prop.
What does it look like?
import React from 'react'
import { CSSProperties } from 'react'
import { ImageProps } from './ImageProps'
class Image extends React.Component<ImageProps> {
render() {
const { source, size } = this.props
const [ width, height ] = size
const style: CSSProperties = { width, height }
return (
<img
style={style}
src={source}
/>
)
}
}
export default Image
Let's take a look
// Size.ts
export type Size = [number, number]
// ImageProps.ts
interface ImageProps {
source: string
size: Size
}
export { ImageProps }
By defining how the component's props are shaped using a TS Interface, we are moving the prop type check at compile-time rather than at run-time.
Intellisense to the rescue
The editor now knows exactly what we are working with (and we can even document it nicely with JSDoc!)
When using TypeScript with ReactJS, we can define the structure of both the component's Props and the component's State using interfaces.
Interfaces will make the component easier to work with, and can even be used as part of the component's documentation!
Best of all, if we do something wrong we'll know it straight away and not later, while running the app in our browser
Just like with React, we can use TypeScript to enhance our Redux Developer experience
Redux comes with TypeScript typings built-in, so we have TS support literally out the box!
Action Types can be defined as literal enums:
enum CounterActionsTypes {
INCREMENT = 'COUNTER@INCREMENT',
RESET = 'COUNTER@RESET'
}
Actions can be shaped as interfaces
interface CounterAction {
type: CounterActionTypes
payload?: any
}
Action Creators are function returning a specific type of Action
const increment: ActionCreator<CounterAction> = function() {
return {
type: CounterActionTypes.INCREMENT
}
}
const reset: ActionCreator<CounterAction> = function() {
return {
type: CounterActionTypes.RESET
}
}
Our state can be defined as an interface
interface CounterState {
count: number
}
const defaultState: CounterState = {
count: 0
}
Our reducer is a pure function, returning a specific type of state
const counterReducer: Reducer<CounterState> = (
state: CounterState = defaultState,
action: CounterAction
) => {
switch (action.type) {
case CounterActionTypes.INCREMENT: {
return {
...state,
count: state.count + 1
}
}
case CounterActionTypes.RESET: {
return {
...state,
count: defaultState.count
}
}
default: {
return state
}
}
}
Our store also has a strong definition
interface ApplicationState {
counter: CounterState
}
const reducers: ReducersMapObject = {
counter: counterReducer
}
const store: Store<ApplicationState> =
createStore<ApplicationState>(combineReducers(reducers))
Many libraries include their .d.ts definitions inside their main npm package, supporting TypeScript out of the box
For those that don't, try:
user@machine$ npm install @types/<library-name>
# npm install @types/react
# npm install @types/react-dom
# npm install @types/react-redux
Using TypeScript in an existing project is possible - although a little bit tedious
We "just" need to add TypeScript to our toolchain and change the extensions of (all?) our files from js(x) to ts(x)
Then we need to fix all errors TypeScript is going to show us, for each of our files!
# ... make a branch (you'll break everything :P)
user@machine$ git checkout -b typescript-integration
# ... add the required packages and some types
user@machine$ yarn add -D typescript ts-loader @types/react @types/react-dom
# ... initialize TypeScript
user@machine$ npx tsc --init --target es2016 --module es2015 --jsx preserve --moduleResolution\
node --allowSyntheticDefaultImports --allowJs
1. Go to your React project's root folder and...
2. Edit your Webpack configuration
//...
module: {
rules: [{
test: /\.tsx?/,
exclude: /node_modules/,
use: ['babel-loader', 'ts-loader']
}]
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
}
//...
3. Change your extensions from .js(x) to .ts(x)
.tsx is required to use the JSX syntax
Thank you!