Pragmatic typed immutable.js records using typescript 2.1+
December 9, 2016 ยท View on GitHub
// Pragmatic typed immutable.js records using typescript 2.1+ // Comment with any suggestions/improvements!
import * as fs from 'fs' import { Record, Map } from 'immutable'
type Stats = fs.Stats;
// Define the basic shape. All properties should be readonly. This model // defines a folder because it seemed easy C:
interface State { readonly path: string; readonly error: string | null; readonly contents: Map<string, Stats>; }
type PartialState = Partial
// Manually define the immutable mutators and accessors you need. All dot
// property accessors are automatically provided by extending from State.
// With the introduction of index types in 2.1, the basic set and update
// operators are easy to set up.
//
// For deep operations, some delicate boilerplate is still necessary.
// Delicate because the definitions are not type-safe, but their usage is.
// That means you need to be very careful here, but the benefit is that
// using them is totally safe if you did everything right.
//
// Usually your state isn't going to be used in every way that immutable
// allows, so you can constrain what you implement to whatever subset you
// need. In practice it honestly isn't too bad, especially if you're decent
// with your editor and multi-line selection.
type Updater
export interface IState extends State {
// All basic operations can now be defined with index types!!!
set
// Deep operations still need to be manually defined. getIn(keyPath: ['contents', string]): Stats | undefined; setIn(keyPath: ['contents', string], value: Stats): IState; deleteIn(keyPath: ['contents', string]): IState;
withMutations(mutator: (s: IState) => any): IState;
// Merge is made easy using typescript's new mapped types!!! merge(partial: PartialState): IState; mergeDeep(partial: PartialState): IState;
}
// Create the record class, using State to typecheck the default values.
// Remember that optional properties need to be explicitly specified as
// undefined or else the record won't acknowledge them down the line. For
// this reason I prefer using unions with null instead of optional
// properties.
const defaultState: State = { path: '', error: null, contents: Map<string, Stats>(), } const RecordClass = Record(defaultState, 'StateRecord');
// If this is top-level state, you may only want to expose initial state.
export const initialState = new RecordClass() as any as IState;
// If you're going to be creating multiple instances, you should export a // constructor. This can be done by forcibly asserting the RecordClass to // another function with our custom state type as the output.
type Constructor
export const StateRecord = RecordClass as any as Constructor
// If you don't want to make use of defaults, you can use the State
// interface directly as the input type.
export const StateRecordWithNoDefaults = RecordClass as any as Constructor
// You can even specify a subset of keys that you want to be required and // optional, though this is getting pretty spicy.
type Input<T, Required extends keyof T, Optional extends keyof T> = { [R in Required]: T[R]; } & { [O in Optional]?: T[O]; }
type StateInput = Input<State, 'path', 'contents' | 'error'>;
export const StateRecordWithSpicyInput = RecordClass as any as Constructor
// Also when you're going to be making multiple records it's usually
// convenient to export a type with the same name as your constructor
// function; you're importing less and defining collections of your record
// looks nicer.
//
// - import { FileRecord, IFile } from './state'
// + import { FileRecord } from './state'
// - let s = new Set
export type StateRecord = IState;