-
-
Save piq9117/e84dd0b47785fbab1019f1915e7374b4 to your computer and use it in GitHub Desktop.
Pragmatic typed immutable.js records using typescript 2.1+
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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<State>; | |
// 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<T> = (value: T) => T; | |
export interface IState extends State { | |
// All basic operations can now be defined with index types!!! | |
set<K extends keyof State>(key: K, value: State[K]): IState; | |
update<K extends keyof State>(key: K, updater: Updater<State[K]>): IState; | |
// 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<TInput> = { | |
(input: TInput): IState; | |
new (input: TInput): IState; | |
} | |
export const StateRecord = RecordClass as any as Constructor<PartialState>; | |
// 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<State>; | |
// 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<StateInput>; | |
// 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<IFile>() | |
// + let s = new Set<FileRecord>() | |
// s.add(new FileRecord(/* whatever */)) | |
export type StateRecord = IState; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment