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