The wonders of TypeScript type system

@krzkaczor

Types IN JavaScript

var something = undefined // type "undefined"
something = "some string" // type "string"
something = 42            // type "number"
function printName(somethingHavingName) {
    console.log(somethingHavingName.name);
}

Duck typing

printName({ name: "Krzysztof", surname: "Kaczor" });
printName(window);
printName({});

Types IN Typescript

var something: string | number | undefined = undefined
something = "some string"
something = 42

Structural type system (ts, flow)

Nominal type system (java, c#)

class A {
    var1: string;   
}

class B {
    var1: string;
}

let a: A = new A(); // type: A
let b: B = new B(); // type: B

a = b; // still type A
b = a; // still type B
class A {
    String var1;   
}

class B {
    String var1;
}

A a = new A();
B b = new B();

a = b; // error: incompatible types: B cannot be converted to A
b = a;
interface SomethingHavingAName {
 name: string;
}

function printName(somethingHavingName: SomethingHavingAName) {
    console.log(somethingHavingName.name);
}
const person = { name: "Krzysztof", surname: "Kaczor" };
printName(person);
printName(window);
printName({}); // compile time error

STRUCTURAL TYPING 

(AKA COMPILE TIME DUCK TYPING)

Redux 101

TODO MVC 💩

(without too much boilerplate)

Actions

Flux Standard Action

{
  type: 'ADD_TODO',
  payload: {
    text: 'Do something.'  
  }
}

redux-utilities/flux-standard-action

Typesafe action representation

export interface AppAction {
  type: string;
  payload?: any;
}
export interface AddTodoAction extends AppAction {
  type: "ADD_TODO";
  payload: {
    task: string;  
  };
}

note: literal type

JS action creators

export function addTodoAction(task: string): AddTodoAction {
    return {
      type: 'ADD_TODO',
      payload: {
        task,  
      }
    }
}

addTodoAction("Wroclaw React presentation")

Auto-generated​ action creators

export type ActionType<T extends AppAction> = T["type"];
export type ActionPayload<T extends AppAction> = T["payload"];
// generics as functions
export function makeActionCreator<T extends AppAction>(
  type: ActionType<T>,
): (p: ActionPayload<T>) => T {
  return payload =>
    ({
      type,
      payload,
    } as T);
}
ActionPayload<IAddTodoAction> === { task: string } // type eq

Auto-generated​ action creators

export const createAddTodoAction = makeActionCreator<AddTodoAction>(
  "ADD_TODO",
);

createAddTodoAction({ task: "react wroclaw presentation" });

Reducers

export const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          task: action.payload.task,
          completed: false,
        }
      ]
    default:
      return state
  }
}

JS reducers

Typesafe state

export interface Todo {
  task: string;
  completed: boolean;
}
export type TodoState = Array<Todo>;

Typesafe immutable state

export interface Todo {
  readonly task: string;
  readonly completed: boolean;
}
export type TodoState = ReadonlyArray<Todo>;
let todos: TodoState;

// ERROR in compile time
todos.push({
    task: "some task",
    completed: false,
}) 


// ERROR in compile time
todos[0].completed = true;

Typesafe immutable state for lazy people

export interface Todo {
  task: string;
  completed: boolean;
}
// mapped types FTW!
type Readonly<T> = {
    readonly [K in keyof T]: T[K];
}
export type TodoState = ReadonlyArray<Readonly<Todo>>;
import { DeepReadonly } from "ts-essentials";

type ReadonlyState = DeepReadonly<Array<Todo>>;

Typesafe reducer ver 1.0

export const todosReducer = (state: TodoState = [], action: any): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          task: action.payload.task,
          completed: false,
        }
      ]
    default:
      return state
  }
}

GLOBAL Union action type

export type AppActionTypes =
  | AddTodoAction
  // ... other actions
  ;
export type AppActionTypes =
  | { type: "ADD_TODO"; payload: { task: string; }; }
  | { type: "REMOVE_TODO"; payload: { taskId: number; }; }
  // ... other actions
  ;

CONTROL FLOW BASED ANALYSIS

const x: string | number = "test123";

if (typeof x === "string") {
  // we are sure here that x is string
} else {
  // and here that it's number
  console.log(x)
}
export const todosReducer = (state: TodoState = [], action: AppActionTypes): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          task: action.payload.task,
          completed: false,
        }
      ]
    default:
      return state
  }
}

Typesafe reducer 💪

App state

interface IAppState {
    todos: TodoState;
    // other combined reducers states
}
const state2props = (state: IAppState) => ({
    firstTodo: state.todos[0],
})

Automatic app state derivation

const appReducer = combineReducers({
    todos: todosReducer,
    // other reducers
})
const appReducers = {
    todos: todosReducer,
    // other reducers
}
export type AppReducer<S> = (
  state: DeepReadonly<S> | undefined,
  action: AppActionTypes,
) => DeepReadonly<S>;

// base on reducers we can infer type of app state
type ReducersMap<T> = { [P in keyof T]: AppReducer<T[P]> };
type ReducersMapToReturnTypes<T> = T extends ReducersMap<infer U> ? U : never;
export type AppState = ReducersMapToReturnTypes<typeof appReducers>;

JS object -> types

👍

Revisting actions

export const createAction = <T extends string, P extends {}>(type: T, payload: P) => {
  return { type, payload };
};

export const actions = {
  loadJWT: (jwt: string) => createAction("AUTH_LOAD_JWT", { jwt }),
  loadTodo: (todo: string) => createAction("LOAD_TODO", { todo }),
};
type ActionCreators = typeof actions;

type allActions = ReturnType<ActionCreators[keyof ActionCreators]>;

Further reading

- intersection types 

- nullable types

- type guards

- regex based literal types (future)

THANKS!

krzkaczor/ts-essentials

@krzkaczor

Wrocław Blockchain
Meetup

THE WONDERS OF TYPESCRIPT TYPE SYSTEM

By krzkaczor

THE WONDERS OF TYPESCRIPT TYPE SYSTEM

  • 637