Skip to content

Instantly share code, notes, and snippets.

@heyimalex
Last active November 23, 2021 23:45
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save heyimalex/099922105b83bacfb69a30989e1fa086 to your computer and use it in GitHub Desktop.
Save heyimalex/099922105b83bacfb69a30989e1fa086 to your computer and use it in GitHub Desktop.
Pragmatic typed immutable.js records using typescript 2.1+
// 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