Skip to content

Instantly share code, notes, and snippets.

@TClark1011
Last active April 15, 2025 05:48
Show Gist options
  • Save TClark1011/81946674fb02eea5a535957c5a864c02 to your computer and use it in GitHub Desktop.
Save TClark1011/81946674fb02eea5a535957c5a864c02 to your computer and use it in GitHub Desktop.
TS - Helpful Utilities
// branded types to allow for better type inference
// with default generic types
/* eslint-disable @typescript-eslint/naming-convention */
type NO_PAYLOAD = {
JgJES6BF8uyaOwF1: "FY7eBhPYJlqOxuVp";
};
type OPTIONAL_PAYLOAD = {
A7nWdXs0r5RLuHRf: "zPcrRNRIl4r5IHbA";
};
/* eslint-enable @typescript-eslint/naming-convention */
// Wrap your payload type in this if you want it to be optionalß
export type OptionalPayload<Payload> = Payload & OPTIONAL_PAYLOAD;
type ExtractOptionalPayloadType<Payload> = Payload extends OptionalPayload<
infer Type
>
? Type
: Payload;
// If only type is provided, there will be no payload field.
// Wrap payload with `OptionalPayload` generic type to make
// it optional
export type Action<
Type extends string,
Payload = NO_PAYLOAD,
> = Payload extends NO_PAYLOAD
? {
type: Type;
}
: Payload extends OPTIONAL_PAYLOAD
? {
type: Type;
payload?: ExtractOptionalPayloadType<Payload>;
}
: Required<{
type: Type;
payload: ExtractOptionalPayloadType<Payload>;
}>;
type EventListenerAdderWithRemover = <
EventName extends keyof (HTMLElementEventMap | DocumentEventMap),
>(
element: HTMLElement | Document | undefined,
type: EventName,
callback: (e: (HTMLElementEventMap | DocumentEventMap)[EventName]) => void,
options?: AddEventListenerOptions | boolean,
) => () => void;
// Adds an event listener to an element and returns a callback
// to remove that event listener
export const addEventListenerWithRemover: EventListenerAdderWithRemover = (
element,
eventName,
callback,
options,
) => {
element?.addEventListener(eventName, callback as never, options);
return () =>
element?.removeEventListener(eventName, callback as never, options);
};
/**
* Index of the middle item of an array. If array contains
* an even number of items, the index of the first middle item
* is returned.
*
* If the array is empty, undefined is returned.
*/
export const getMiddleIndexOfArray = (arr: unknown[]): number | undefined => {
if (!arr.length) return undefined;
return Math.floor((arr.length - 1) / 2);
}
export const getMiddleItemFromArray = <Item>(arr: Item[]): Item | undefined => {
const middleIndex = getMiddleIndexOfArray(arr);
if (middleIndex === undefined || middleIndex < 0) return undefined;
const middleItem = arr[middleIndex];
return middleItem;
};
type Comparator<Item> = (a: Item, b: Item) => boolean;
const shallowEqual: Comparator<unknown> = (a, b) => a === b;
export const toggleArrayElement = <Item>(
arr: Item[],
target: Item,
compare: Comparator<Item> = shallowEqual
): Item[] => {
const isEqualToTarget = (i: Item) => compare(i, target);
const existingItem = arr.find(isEqualToTarget);
if (!existingItem) return [...arr, target];
const withoutTarget = arr.filter((item) => !isEqualToTarget(item));
return withoutTarget;
};
const randomizedComparison = () => {
const floatBetweenZeroAndTwo = Math.random() * 2;
const integerBetweenZeroAndTwo = Math.round(floatBetweenZeroAndTwo);
const integerBetweenNegativeOneAndOne = integerBetweenZeroAndTwo - 1;
return integerBetweenNegativeOneAndOne;
};
export const shuffleArray = <Item>(arr: Item[]): Item[] =>
[...arr].sort(randomizedComparison);
export const getRandomArrayItem = <Item>(arr: Item[]): Item | undefined => {
const shuffled = shuffleArray(arr);
const randomItem = shuffled[0];
return randomItem;
};
export const sortAccordingToReferenceArray = <Item, Reference>(
items: Item[],
referenceArr: Reference[],
comparator: (item: Item, reference: Reference) => boolean
) =>
[...items].sort((a, b) => {
const aIndex = referenceArr.findIndex((reference) =>
comparator(a, reference)
);
const bIndex = referenceArr.findIndex((reference) =>
comparator(b, reference)
);
if (aIndex === -1) {
return 1;
}
if (bIndex === -1) {
return -1;
}
return aIndex - bIndex;
});
const _ = Symbol();
type YOU_FORGOT<MissingTypes> = {
ERROR: "DOES NOT CONTAIN ALL TYPES";
missingTypes: MissingTypes;
_: typeof _;
};
// When actually used with `arrayOfAll` the IDE should
// flatten this out in the error pop up window to actually
// list out the forgotten types, eg;
// `YOU_FORGOT<"a" | "b">` if "a" and "b" were left out
// of the array.
export const arrayOfAll =
<T>() =>
<U extends T[]>(
array: U &
([T] extends [U[number]]
? unknown
: YOU_FORGOT<Exclude<T, U[number]>>) & {
0: T;
},
) =>
array;
// Takes a number and wraps it around if it goes beneath
// a specified minimum or over a specified maximum.
const clampWithWrap = (value: number, min: number, max: number): number => {
const howFarOverMax = value - max;
const howFarUnderMin = min - value;
if (value > max) {
return clampWithWrap(min + howFarOverMax - 1, min, max);
}
if (value < min) {
return clampWithWrap(max - howFarUnderMin + 1, min, max);
}
return value;
};
export default clampWithWrap;

CLI Caching

To setup simple caching in a CLI app, use the packages flat-cache and find-cache-dir.

const APP_NAME = "your-app-name-here";
const cacheDirectory = findCacheDir({ name: APP_NAME });
if (!cacheDirectory) throw new Error("Cache directory not found");

const cache = cacheManager.load(APP_NAME, cacheDirectory);

cache.get("your-key-here");
cache.set("your-key-here", "your-value-here");

cache.save();
const coerceIntoArray = <Item>(val: Item | Item[]): Item[] =>
Array.isArray(val) ? val : [val];
export default coerceIntoArray;
/**
* Allows you to apply color to a message within `console.log`
*
* The below example will log the message "Hello World" in
* green.
*
* @example
* console.log(...colorConsoleMessage('Hello World', 'green'))
*/
const colorConsoleMessage = (
message: string,
color: string
): [string, string] => [`%c${message}`, `color: ${color};`];
export default colorConsoleMessage;
const composeDummyImageSrc = (width: number, height = width) =>
`https://picsum.photos/${width}/${height}?key=${Math.random()}`;
export default composeDummyImageSrc;
const composeQuantityStatement = (
items: any[] | readonly any[] | number,
label: string,
pluralLabel?: string
) => {
const quantity = Array.isArray(items) ? items.length : items;
const finalPluralLabel = pluralLabel ?? `${label}s`;
const finalLabel = quantity === 1 ? label : finalPluralLabel;
return `${quantity} ${finalLabel}`;
};
export default composeQuantityStatement;
// Can be used to add a prop to an object you are
// defining if the value for that prop is not falsy
const conditionalProp = <Value, Key extends string>(
value: Value,
key: Key,
): Record<Key, Value> | {} => (value ? { [key]: value } : {});
//Example usage:
const folder = {
name: "Documents",
files: folderFiles,
...conditionalProp(folderFiles.length, "fileCount"),
// "fileCount" will be added to the object with
// a values equal to `folderFiles.length` only if
// `folderFiles.length` is greater than 0
};
export default conditionalProp;
//Converts a css style object (like those seen in
// React inline styles) to a css string.
//
// NOTE: this does not convert multi-word properties
// to kebab case, you will need to do that yourself
// if your object has multi-word properties.
const convertStyleObjectToString = (
obj: Record<string, string | number>,
): string =>
Object.entries(obj)
.map(([key, value]) => `${key}: ${value}`)
.join(";");
export default convertStyleObjectToString;
/**
* Designed to help logging in long functions that have multiple
* steps. Every log made with a progress logger will be prefixed
* with a number that increases on every call, so you can easily
* keep track of the progress of your function.
*
* The intended way to use this is to initalize a logger at the
* start of a function, and then use it to log every step.
*/
const createProgressLogger = (name: string) => {
let timesCalled = 0;
//NOTE: If you want to temporarily turn off the logging to
//stop the console getting filled up, you can just set the
//enabled property to default to false. You can also
//comment out the console.log line in the log function.
let enabled = true;
return {
log: (...params: any) => {
if (!enabled) return;
timesCalled += 1;
console.log(`${timesCalled}. (${name}) `, ...params);
},
setEnabled: (value: boolean) => {
enabled = value;
},
};
};
/**
* If your project uses the eslint rule 'no-param-reassign',
* then it may complain when you reassign properties on the
* draft object. An easy solution to this is to use the
* `ignorePropertyModificationsForRegex` option on the rule
* to allow any params with `draft` in the name to be reassigned.
*
* ```
* 'no-param-reassign': ['error', {
* props: true,
* ignorePropertyModificationsForRegex: ['\\w*[Dd]raft\\w*']
* }]
* ```
*/
type DeepMutable<T> = T extends readonly (infer U)[]
? DeepMutable<U>[]
: T extends object
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
: T;
type Producer<T> = (draft: DeepMutable<T>) => void;
/**
* An extremely bare bones implementation of the `produce`
* function from the immer library.
*
* NOTE: If any of the properties in the object are enforced
* as readonly (eg; with `Object.freeze`), this function will
* not work as expected and will likely throw an error.
*/
export function produce<T>(producer: Producer<T>): (data: T) => T;
export function produce<T>(data: T, producer: Producer<T>): T;
export function produce<T>(
...params: [T, Producer<T>] | [Producer<T>]
): T | ((data: T) => T) {
if (params.length === 2) {
const [data, producer] = params;
const draft = structuredClone(data);
if (Object.isFrozen(draft)) {
throw new Error("Cannot produce on a frozen object");
}
producer(draft as DeepMutable<T>);
return draft;
}
const [producer] = params;
return (data) => produce(data, producer);
}
type Primitive = string | number | boolean | null | undefined;
const isPrimitive = (value: unknown): value is Primitive =>
typeof value !== "object" || value === null;
/**
* NOTE: If you just need to do a deep check to see if an object contains
* any instance of a primitive value like a string or number, you can just
* stringify the object and check if the string contains the value.
*/
export const deepFindInObject = <TheObject>(
obj: TheObject,
checker: (value: unknown) => boolean,
): boolean => {
if (isPrimitive(obj)) return checker(obj);
const entries = Object.entries(obj);
for (const [key, value] of entries) {
if (checker(key) || deepFindInObject(value, checker)) return true;
}
return false;
};
import { ZodType, ZodTypeAny, z } from "zod";
/* eslint-disable arrow-body-style */
export {};
/* #region UTIL TYPES */
type ValueOf<T> = T[keyof T];
type LoosenTuple<BaseTuple extends readonly unknown[]> = ValueOf<BaseTuple>[];
type AtLeast2<T> = [T, T, ...T[]];
/* #endregion */
type AnyFlags = Record<string, readonly string[]>;
type InferMultiFlagSettingTuple<
Flags extends AnyFlags,
SpecificFlagsTuple extends [keyof Flags, keyof Flags, ...(keyof Flags)[]],
> = SpecificFlagsTuple extends [
infer FlagA extends keyof Flags,
infer FlagB extends keyof Flags,
]
? [ValueOf<Flags[FlagA]>, ValueOf<Flags[FlagB]>]
: SpecificFlagsTuple extends [
infer FlagA extends keyof Flags,
infer FlagB extends keyof Flags,
infer FlagC extends keyof Flags,
]
? [ValueOf<Flags[FlagA]>, ValueOf<Flags[FlagB]>, ValueOf<Flags[FlagC]>]
: // Keep going to support more tuple lengths...
never;
type EnvVariableRawValue = string | null | undefined;
type SpecificFlagSettingSelector<
Flags extends AnyFlags,
SpecificFlag extends keyof Flags,
> = LoosenTuple<Flags[SpecificFlag]> | "all" | ValueOf<Flags[SpecificFlag]>;
type FlagSettingSelection<
Flags extends AnyFlags,
SpecificFlag extends keyof Flags,
> = {
flag: SpecificFlag;
settings: SpecificFlagSettingSelector<Flags, SpecificFlag>;
};
type SingleFlagVariableValueDefinition<
Flags extends AnyFlags,
SpecificFlag extends keyof Flags,
> = {
for: FlagSettingSelection<Flags, SpecificFlag>;
value: EnvVariableRawValue;
};
type MultiFlagVariableValueDefinition<
Flags extends AnyFlags,
SpecificFlagsTuple extends AtLeast2<keyof Flags>,
> = {
for: {
flags: SpecificFlagsTuple;
settings: InferMultiFlagSettingTuple<Flags, SpecificFlagsTuple>;
};
value: EnvVariableRawValue;
};
const composeSimpleVariableValueDefinition = <
Flags extends AnyFlags,
SpecificFlag extends keyof Flags,
>(
specificFlag: SpecificFlag,
flagSettings: SpecificFlagSettingSelector<Flags, SpecificFlag>,
value: EnvVariableRawValue,
): SingleFlagVariableValueDefinition<Flags, SpecificFlag> => ({
for: {
flag: specificFlag,
settings: flagSettings,
},
value,
});
const composeMultiFlagVariableValueDefinition = <
Flags extends AnyFlags,
const SpecificFlagsTuple extends AtLeast2<keyof Flags>,
>(
specificFlags: SpecificFlagsTuple,
flagSettings: InferMultiFlagSettingTuple<Flags, SpecificFlagsTuple>,
value: EnvVariableRawValue,
): MultiFlagVariableValueDefinition<Flags, SpecificFlagsTuple> => ({
for: {
flags: specificFlags,
settings: flagSettings,
},
value,
});
type VariableRules<Flags extends AnyFlags, Schema extends ZodTypeAny> = {
name: string;
requiredIn:
| Partial<{
[SpecificFlag in keyof Flags]: SpecificFlagSettingSelector<
Flags,
SpecificFlag
>;
}>
| "always";
schema?: Schema;
values:
| (
| SingleFlagVariableValueDefinition<Flags, any>
| MultiFlagVariableValueDefinition<Flags, [any, any, ...any[]]>
)[]
| EnvVariableRawValue;
};
const createEnvDefiner = <const Flags extends AnyFlags>(envs: Flags) => {
return <Schema extends ZodTypeAny>(rules: VariableRules<Flags, Schema>) =>
rules;
};
const defineEnv = createEnvDefiner({
DEPLOY_TYPE: ["testing", "production"],
REGION: ["A", "B"],
});
defineEnv({
name: "API_URL",
requiredIn: "always",
schema: z.string().url(),
values: [
composeSimpleVariableValueDefinition(
"DEPLOY_TYPE",
["production"],
"production api url",
),
composeSimpleVariableValueDefinition(
"DEPLOY_TYPE",
["testing"],
"testing api url",
),
],
});
defineEnv({
name: "SUPPORT_EMAIL",
requiredIn: "always",
schema: z.string().email(),
values: "the support email",
});
defineEnv({
name: "ADMIN_USER_GROUP_NAME_POSTFIX",
requiredIn: {
REGION: ["B"],
},
schema: z.string(),
values: "the admin user group name postfix",
});
defineEnv({
name: "ANALYTICS_API_URL",
requiredIn: {
REGION: ["A"],
},
values: [
composeSimpleVariableValueDefinition(
"DEPLOY_TYPE",
"production",
"production analytics api url",
),
composeSimpleVariableValueDefinition(
"DEPLOY_TYPE",
"testing",
"testing analytics api url",
),
],
});
defineEnv({
name: "CONFIG_FILE_ID",
requiredIn: "always",
schema: z.preprocess(Number, z.number().int()),
values: [
composeMultiFlagVariableValueDefinition(
["DEPLOY_TYPE", "REGION"],
["testing", "A"],
"0",
),
composeMultiFlagVariableValueDefinition(
["DEPLOY_TYPE", "REGION"],
["production", "A"],
"1",
),
composeMultiFlagVariableValueDefinition(
["DEPLOY_TYPE", "REGION"],
["testing", "B"],
"2",
),
composeMultiFlagVariableValueDefinition(
["DEPLOY_TYPE", "REGION"],
["production", "B"],
"3",
),
],
});
// # Test
type TestFlags = {
DEPLOY_TYPE: readonly ["testing", "production"];
REGION: readonly ["A", "B"];
};
type A = InferMultiFlagSettingTuple<TestFlags, ["DEPLOY_TYPE", "REGION"]>;
import produce from "immer";
import {
WritableAtom,
atom,
PrimitiveAtom,
Getter,
Setter,
Atom,
useAtomValue,
getDefaultStore,
useSetAtom,
} from "jotai";
import { useMemo } from "react";
export const createStateAtom = <State>(initialValue: State) =>
atom(initialValue);
type CreateActionAtomScopedParams<State, OtherParams extends any[]> = [
theAtom: PrimitiveAtom<State>,
createNextValue: (
currentState: State,
...otherParams: OtherParams
) => State | void,
];
type CreateActionAtomWideParams<OtherParams extends any[]> = [
setter: (get: Getter, set: Setter, ...otherParams: OtherParams) => void,
];
export function createActionAtom<State, OtherParams extends any[]>(
...params: CreateActionAtomScopedParams<State, OtherParams>
): WritableAtom<null, OtherParams, void>;
export function createActionAtom<OtherParams extends any[]>(
...params: CreateActionAtomWideParams<OtherParams>
): WritableAtom<null, OtherParams, void>;
export function createActionAtom(
...args:
| CreateActionAtomScopedParams<any, any[]>
| CreateActionAtomWideParams<any[]>
) {
if (args.length === 1) {
const [setter] = args;
return atom(null, setter);
} else {
const [theAtom, createNextValue] = args;
return atom(null, (get, set, ...params) => {
const currentState = get(theAtom);
const nextState = produce(currentState, (draft: any) =>
createNextValue(draft, ...params),
);
set(theAtom, nextState);
});
}
}
// # useActionAtom
export const useActionAtom = useSetAtom;
// # createSelectorAtom
// TODO: "wide" selectors that can access multiple atoms
type CreateSelectorAtomScopedParams<
State,
OtherParams extends any[],
Result,
> = [
theAtom: PrimitiveAtom<State>,
selector: (state: State, ...otherParams: OtherParams) => Result,
];
export function createSelectorAtom<State, OtherParams extends any[], Result>(
...params: CreateSelectorAtomScopedParams<State, OtherParams, Result>
): (...otherParams: OtherParams) => Atom<Result>;
export function createSelectorAtom<State, Result>(
...params: CreateSelectorAtomScopedParams<State, [], Result>
): Atom<Result>;
export function createSelectorAtom(
...[theAtom, selector]: CreateSelectorAtomScopedParams<any, any[], any>
) {
const isParameterizedSelector = selector.length > 1;
if (isParameterizedSelector) {
return atom((get) => selector(get(theAtom)));
}
return (...otherParams: any[]) =>
atom((get) => selector(get(theAtom), ...otherParams));
}
// # useSelectorAtom
type UseSelectorAtomSimpleParams<Result> = [theAtom: Atom<Result>];
type UseSelectorAtomAdvancedParams<Params extends any[], Result> = [
callbackAtom: (...params: Params) => Atom<Result>,
...params: Params,
];
export function useSelectorAtom<Result>(
...params: UseSelectorAtomSimpleParams<Result>
): Result;
export function useSelectorAtom<Params extends any[], Result>(
...otherParams: UseSelectorAtomAdvancedParams<Params, Result>
): Result;
export function useSelectorAtom(
...args:
| UseSelectorAtomSimpleParams<any>
| UseSelectorAtomAdvancedParams<any[], any>
) {
const [theAtomOrAtomCreator, ...otherParams] = args;
const theAtom = useMemo(() => {
if (typeof theAtomOrAtomCreator === "function") {
return theAtomOrAtomCreator(...otherParams);
}
return theAtomOrAtomCreator;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [theAtomOrAtomCreator, ...otherParams]);
return useAtomValue(theAtom);
}
// # Helper Functions
export const atomStore = getDefaultStore();
export const subscribeToAtom = <State>(
theAtom: Atom<State>,
callback: (newState: State) => void,
) => {
atomStore.sub(theAtom, () => callback(atomStore.get(theAtom)));
};
//# Example Usage
const numbersAtom = createStateAtom([] as number[]);
const numberIndexToDeleteAtom = createStateAtom(null as null | number);
// Basic Action Atom...
const clearNumbersAtom = createActionAtom(numbersAtom, () => []);
// Action Atom With Parameters
const deleteNumberAtIndexAtom = createActionAtom(
numbersAtom,
(state, index: number) => {
state.splice(index, 1);
},
);
// Access multiple atom within an action
const deleteTargetNumberAtom = createActionAtom((get, set) => {
const targetIndex = get(numberIndexToDeleteAtom);
if (!targetIndex) return;
set(deleteNumberAtIndexAtom, targetIndex);
});
const isEven = (n: number) => n % 2 === 0;
// Basic Selector Atom
const selectEvenNumbersAtom = createSelectorAtom(numbersAtom, (numbers) =>
numbers.filter(isEven),
);
// Selector Atom With Parameters
const selectNumberAtIndexAtom = createSelectorAtom(
numbersAtom,
(numbers, targetIndex: number) => numbers[targetIndex],
);
// Using a Basic Selector Atom
const useA = () => useSelectorAtom(selectEvenNumbersAtom);
// Using a Selector Atom With Parameters
const useB = () => useSelectorAtom(selectNumberAtIndexAtom, 0);
/* eslint-disable no-param-reassign */
/* eslint-disable no-underscore-dangle */
import { Draft, produce } from "immer";
import { type Reducer } from "react";
export type Action<Name extends string> = {
type: Name;
};
export type PayloadAction<Name extends string, Payload> = Action<Name> & {
payload: Payload;
};
type CaseHandler<State, TheAction> = (
state: Draft<State>,
action: TheAction,
) => void;
type ReducerBuilder<State, TheAction> = {
addCase<NewActionName extends string>(
name: NewActionName,
handler: CaseHandler<State, Action<NewActionName>>,
): ReducerBuilder<State, TheAction | Action<NewActionName>>;
addPayloadCase<NewActionName extends string>(
name: NewActionName,
): <Payload>(
handler: CaseHandler<State, PayloadAction<NewActionName, Payload>>,
) => ReducerBuilder<State, TheAction | PayloadAction<NewActionName, Payload>>;
/**
* Middleware will be executed before the action is handled by the
* reducer
*/
addMiddleware: (
middleware: (state: State, action: TheAction) => void,
) => ReducerBuilder<State, TheAction>;
build: () => Reducer<State, TheAction>;
};
type ReducerCase<
State,
TheActionType extends string,
TheAction extends Action<TheActionType>,
> = {
type: TheActionType;
handler: CaseHandler<State, TheAction>;
};
type EasyToWorkWithReducerCase<State> = ReducerCase<
State,
string,
Action<string>
>;
const _reducerBuilder = <State, TheAction extends Action<string>>(
cases: EasyToWorkWithReducerCase<State>[],
middlewareCallbacks: ((state: State, action: TheAction) => void)[] = [],
): ReducerBuilder<State, TheAction> => {
const reducer = (state: State, action: TheAction): State => {
const correspondingCase = cases.find(({ type }) => type === action.type);
if (!correspondingCase)
throw new Error(`received unknown action type "${action.type}"`);
middlewareCallbacks.forEach((callback) => {
callback(state, action);
});
const newState = produce(state, (draftState) => {
correspondingCase.handler(draftState, action);
});
return newState;
};
const warnAboutPrematureMiddleware = () => {
console.warn(
"You have added a case to a reducer after adding middleware. It is recommended that middleware is added last.",
);
};
const addCase = <NewActionName extends string>(
name: NewActionName,
handler: CaseHandler<State, Action<NewActionName>>,
): ReducerBuilder<State, TheAction | Action<NewActionName>> => {
if (middlewareCallbacks.length > 0) warnAboutPrematureMiddleware();
return _reducerBuilder([
...cases,
{
type: name,
handler: handler as any,
},
]);
};
const addPayloadCase =
<NewActionName extends string>(name: NewActionName) =>
<Payload>(
handler: CaseHandler<State, PayloadAction<NewActionName, Payload>>,
): ReducerBuilder<
State,
TheAction | PayloadAction<NewActionName, Payload>
> => {
if (middlewareCallbacks.length > 0) warnAboutPrematureMiddleware();
return _reducerBuilder([
...cases,
{
type: name,
handler: handler as any,
},
]);
};
const addMiddleware: ReducerBuilder<State, TheAction>["addMiddleware"] = (
middlewareCallback,
) => _reducerBuilder(cases, [...middlewareCallbacks, middlewareCallback]);
return {
addCase,
addPayloadCase,
build: () => reducer,
addMiddleware,
};
};
export const reducerBuilder = <State>() => _reducerBuilder<State, never>([]);
export const middlewareForActionType =
<
State,
TheAction extends Action<string>,
AllowedTypes extends TheAction["type"],
>(
types: AllowedTypes[],
callback: (
state: State,
action: Extract<TheAction, { type: AllowedTypes }>,
) => void,
) =>
(state: State, action: TheAction) => {
if (types.includes(action.type as AllowedTypes)) {
callback(state, action as any);
}
};
export const middlewareForAllActionsExcept =
<
State,
TheAction extends Action<string>,
DisallowedTypes extends TheAction["type"],
>(
types: DisallowedTypes[],
callback: (
state: State,
action: Exclude<TheAction, { type: DisallowedTypes }>,
) => void,
) =>
(state: State, action: TheAction) => {
if (!types.includes(action.type as DisallowedTypes)) {
callback(state, action as any);
}
};
type CounterState = {
value: number;
};
const counterReducer = reducerBuilder<CounterState>()
.addCase("increment", (state) => {
state.value += 1;
})
.addPayloadCase("add")<number>((state, { payload }) => {
state.value += payload;
})
.addMiddleware((_, action) => {
// Runs on all
console.log("middleware", action);
})
.addMiddleware(
middlewareForActionType(["add", "increment"], (state, action) => {}),
)
.addMiddleware(middlewareForAllActionsExcept(["add"], (state, action) => {}))
.addMiddleware(
middlewareForAllActionsExcept(["increment"], (state, action) => {}),
)
.build();
counterReducer(
{
value: 0,
},
{
type: "add",
payload: 5,
},
);
/**
* This a function to help with situations where you
* need to fetch data in linear stages (eg; you fetch A,
* then use data from A to fetch B, and then use data from
* B to fetch C).
*
* This function should automatically throw if any of the
* stages returns null or undefined, and if an error occurs,
* the outputs from all prior stages will be included in
* the error for debugging purposes.
*
* You can also provide a side effect function that will
* run in between each stage but cannot effect the result.
*/
type AnyRecord = Record<string, any>;
class LinearStagedDataFetchingError extends Error {
baseError: Error;
stageOutputs: AnyRecord;
failedAtStageIndex: number;
constructor(
baseError: Error,
stageOutputs: AnyRecord,
failedAtStageIndex: number,
) {
super(baseError.message);
this.stack = baseError.stack;
this.name = baseError.name;
this.stageOutputs = stageOutputs;
this.failedAtStageIndex = failedAtStageIndex;
this.baseError = baseError;
}
}
type PromiseOrNot<T> = T | Promise<T>;
export async function linearStagedDataFetching<
Stage1Output extends AnyRecord,
Result,
>(
dataFetchingFunctions: [
() => PromiseOrNot<Stage1Output>,
(prev: Stage1Output) => PromiseOrNot<Result>,
],
sideEffect?:
| ((stageIndex: 0) => void)
| ((stageIndex: 1, output: Stage1Output) => void),
): Promise<Result>;
export async function linearStagedDataFetching<
Stage1Output extends AnyRecord,
Stage2Output extends AnyRecord,
Result,
>(
dataFetchingFunctions: [
() => PromiseOrNot<Stage1Output>,
(prev: Stage1Output) => PromiseOrNot<Stage2Output>,
(prev: Stage1Output & Stage2Output) => PromiseOrNot<Result>,
],
sideEffect?:
| ((stageIndex: 0) => void)
| ((stageIndex: 1, output: Stage1Output) => void)
| ((stageIndex: 2, output: Stage1Output & Stage2Output) => void),
): Promise<Result>;
export async function linearStagedDataFetching<
Stage1Output extends AnyRecord,
Stage2Output extends AnyRecord,
Stage3Output extends AnyRecord,
Result,
>(
dataFetchingFunctions: [
() => PromiseOrNot<Stage1Output>,
(prev: Stage1Output) => PromiseOrNot<Stage2Output>,
(prev: Stage1Output & Stage2Output) => PromiseOrNot<Stage3Output>,
(prev: Stage1Output & Stage2Output & Stage3Output) => PromiseOrNot<Result>,
],
sideEffect?:
| ((stageIndex: 0) => void)
| ((stageIndex: 1, output: Stage1Output) => void)
| ((stageIndex: 2, output: Stage1Output & Stage2Output) => void)
| ((
stageIndex: 3,
output: Stage1Output & Stage2Output & Stage3Output,
) => void),
): Promise<Result>;
/**
* TODO: Change signatures of the stage functions so they can
* return undefined or null, but if that happens when running
* the function we just throw an error.
*/
export async function linearStagedDataFetching(
dataFetchingFunctions: any[],
): Promise<any> {
/**
* IMPORTANT: I'm pretty sure this implementation won't actually work,
*/
let stagesOutput: AnyRecord = {};
const result = dataFetchingFunctions.reduce(
async (prevPromise, currentFunction, stageIndex) => {
const prev = await prevPromise;
const current = await currentFunction(prev).catch((err: Error) => {
throw new LinearStagedDataFetchingError(err, stagesOutput, stageIndex);
});
stagesOutput = { ...stagesOutput, ...current };
return current;
},
undefined,
);
return result;
}
/**
* This is the beginning of code for a program that will run testing benchmarks
* to compare the `immer` library with the performance of my own minimal
* re-implementations of the same functionality that uses `structuredClone`
*
* The idea is that we implement a system where we can represent mutations
* as objects and then execute those mutations. We can compare the performance
* of the 2 functions by randomly generating a large piece of data, then generating
* a very large array of mutations that we can execute on the data. Then we run those
* mutations using both libraries and compare the performance.
*/
const BASIC_VALUE_TYPES = [
"string",
"number",
"boolean",
"symbol",
"bigint",
] as const;
const randomBasicValueType = () =>
BASIC_VALUE_TYPES[Math.floor(Math.random() * BASIC_VALUE_TYPES.length)]!;
const randomIntBetween = (min: number, max: number) =>
Math.floor(Math.random() * (max - min + 1)) + min;
const randomString = () => Math.random().toString(36).substring(7);
const randomNumber = () => Math.random() * randomIntBetween(-100000, 100000);
const randomBoolean = () => Math.random() > 0.5;
const randomSymbol = () => Symbol(randomString());
const randomBigInt = () => BigInt(randomIntBetween(-1000000, 1000000));
const randomBasicValue = () => {
const type = randomBasicValueType();
switch (type) {
case "string":
return randomString();
case "number":
return randomNumber();
case "boolean":
return randomBoolean();
case "symbol":
return randomSymbol();
case "bigint":
return randomBigInt();
default:
// @ts-expect-error - if this is red the switch statement is not exhaustive
return type.toString();
}
};
const ADVANCED_VALUE_TYPES = ["object", "array", "map", "set"] as const;
const randomAdvancedValueType = () =>
ADVANCED_VALUE_TYPES[
Math.floor(Math.random() * ADVANCED_VALUE_TYPES.length)
]!;
const generateEntries = (length: number) =>
Array.from({ length }, () => [
randomString(),
randomBasicValue(),
]) as readonly [string, unknown][];
const randomObject = (length?: number) => {
const finalLength = length ?? randomIntBetween(0, 10000);
const entries = generateEntries(finalLength);
return Object.fromEntries(entries);
};
const randomMap = (length?: number) => {
const finalLength = length ?? randomIntBetween(0, 10000);
const entries = generateEntries(finalLength);
return new Map(entries);
};
const randomArray = (length?: number) => {
const finalLength = length ?? randomIntBetween(0, 10000);
const values = Array.from({ length: finalLength }, () => randomBasicValue());
return values;
};
const randomSet = (length?: number) => new Set(randomArray(length));
const randomAdvancedValueTypeLength = () => randomIntBetween(0, 10000);
const randomAdvancedValue = (length?: number) => {
const type = randomAdvancedValueType();
switch (type) {
case "object":
return randomObject(length);
case "array":
return randomArray(length);
case "map":
return randomMap(length);
case "set":
return randomSet(length);
default:
// @ts-expect-error - if this is red the switch statement is not exhaustive
return type.toString();
}
};
const randomStartingValue = () => randomAdvancedValue(1000);
const randomValue = () =>
Math.random() > 0.1 ? randomBasicValue() : randomAdvancedValue();
/* eslint-disable no-param-reassign */
const NO_PAYLOAD = Symbol("NO_PAYLOAD");
type NoPayload = typeof NO_PAYLOAD;
type MutationDataType = "any" | "array" | "list" | "map";
type EmptyObject = Record<string, never>;
type IndividualMutation<
DataType extends MutationDataType,
Operation extends string,
Payload extends NoPayload | unknown = NoPayload,
> = {
dataType: DataType;
operation: Operation;
payload: Payload;
};
// # MUTATIONS
type AnyMutation = IndividualMutation<
"any",
"ASSIGN_FIELD",
{
field: string;
value: unknown;
}
>;
type ListMutation = IndividualMutation<"list", "PUSH", unknown>;
type ArrayMutation =
| IndividualMutation<"array", "POP">
| IndividualMutation<"array", "SHIFT">
| IndividualMutation<"array", "UNSHIFT", unknown>
| IndividualMutation<
"array",
"ASSIGN_INDEX",
{
index: number;
value: unknown;
}
>
| IndividualMutation<"array", "SPLICE", [number, number]>;
type MapMutation = IndividualMutation<
"map",
"MAP_SET",
{
key: string;
value: unknown;
}
>;
type Mutation = AnyMutation | ArrayMutation | ListMutation | MapMutation;
type Operation = Mutation["operation"];
type MutationByOperation<TheOperation extends Operation> = Extract<
Mutation,
{ operation: TheOperation }
>;
const a = new Map();
const determineValidMutationTypesForData = (
data: unknown,
): MutationDataType[] => {
const result: MutationDataType[] = ["any"];
if (Array.isArray(data) || data instanceof Set) {
result.push("list");
}
if (Array.isArray(data)) {
result.push("array");
}
if (data instanceof Map) {
result.push("map");
}
return result;
};
const narrowOperationPayloadType = <TheOperation extends Operation>(
payload: Extract<Mutation, { operation: TheOperation }>["payload"],
) => payload as any;
const generatePayloadForOperation = (operation: Operation): any => {
switch (operation) {
case "ASSIGN_FIELD":
return narrowOperationPayloadType<"ASSIGN_FIELD">({
field: randomString(),
value: randomValue(),
});
case "PUSH":
return randomValue();
case "ASSIGN_INDEX":
return narrowOperationPayloadType<"ASSIGN_INDEX">({
index: randomIntBetween(0, 1000),
value: randomValue(),
});
case "MAP_SET":
return narrowOperationPayloadType<"MAP_SET">({
key: randomString(),
value: randomValue(),
});
case "SPLICE":
return [randomIntBetween(0, 1000), randomIntBetween(0, 1000)];
case "UNSHIFT":
return randomValue();
case "POP":
case "SHIFT":
return NO_PAYLOAD;
default:
// @ts-expect-error - if this is red the switch statement is not exhaustive
return operation.toString();
}
};
export const generateRandomMutationForDataType = (
dataType: MutationDataType,
) => {
switch (dataType) {
case "any":
return {
dataType,
operation: "ASSIGN_FIELD",
payload: generatePayloadForOperation("ASSIGN_FIELD"),
} as AnyMutation;
case "list":
return {
dataType,
operation: "PUSH",
payload: generatePayloadForOperation("PUSH"),
} as ListMutation;
case "array":
return {
dataType,
operation: randomArrayMutation(),
payload: generatePayloadForOperation(randomArrayMutation()),
} as ArrayMutation;
case "map":
return {
dataType,
operation: "MAP_SET",
payload: generatePayloadForOperation("MAP_SET"),
} as MapMutation;
default:
// @ts-expect-error - if this is red the switch statement is not exhaustive
return dataType.toString();
}
};
const executePush = (data: unknown[] | Set<unknown>, payload: unknown) => {
if (Array.isArray(data)) {
data.push(payload);
} else {
data.add(payload);
}
};
const executeMapSet = (
data: Map<string, unknown>,
payload: MutationByOperation<"MAP_SET">["payload"],
) => {
data.set(payload.key, payload.value);
};
const executeMutation = (data: any, mutation: Mutation) => {
switch (mutation.operation) {
case "ASSIGN_FIELD":
data[mutation.payload.field] = mutation.payload.value;
break;
case "PUSH":
executePush(data, mutation.payload);
break;
case "POP":
data.pop();
break;
case "SHIFT":
data.shift();
break;
case "UNSHIFT":
data.unshift(mutation.payload);
break;
case "ASSIGN_INDEX":
data[mutation.payload.index] = mutation.payload.value;
break;
case "SPLICE":
data.splice(mutation.payload[0], mutation.payload[1]);
break;
case "MAP_SET":
executeMapSet(data, mutation.payload);
break;
default:
// @ts-expect-error - if this is red the switch statement is not exhaustive
mutation.toString();
}
};
type ParseRouteSegment<Segment extends string> = Segment extends `:${string}`
? string
: Segment;
// TODO: Handle optional param ('/:id?') being left out of the url
// Experiment on typing what the apps `pathname` could be based on
// all of the routes that have been defined
type PathNameType<RoutePath extends string> =
RoutePath extends `${infer Start}/${infer Rest}`
? `${ParseRouteSegment<Start>}/${PathNameType<Rest>}`
: ParseRouteSegment<RoutePath>;
type A = PathNameType<"post/:id" | "viewMode/:mode" | "user/:userId/edit">;
/* eslint-disable no-underscore-dangle */
import { type Reducer } from "react";
export type Action<Name extends string> = {
type: Name;
};
export type PayloadAction<Name extends string, Payload> = Action<Name> & {
payload: Payload;
};
type ReducerBuilder<State, TheAction> = {
addCase<NewActionName extends string>(
name: NewActionName,
handler: (state: State, action: Action<NewActionName>) => State,
): ReducerBuilder<State, TheAction | Action<NewActionName>>;
addPayloadCase<NewActionName extends string>(
name: NewActionName,
): <Payload>(
handler: (
state: State,
action: PayloadAction<NewActionName, Payload>,
) => State,
) => ReducerBuilder<State, TheAction | PayloadAction<NewActionName, Payload>>;
/**
* Middleware will be executed before the action is handled by the
* reducer
*/
addMiddleware: (
middleware: (state: State, action: TheAction) => void,
) => ReducerBuilder<State, TheAction>;
build: () => (state: State, action: TheAction) => State;
};
type ReducerCase<
State,
TheActionType extends string,
TheAction extends Action<TheActionType>,
> = {
type: TheActionType;
handler: (state: State, action: TheAction) => State;
};
type EasyToWorkWithReducerCase<State> = ReducerCase<
State,
string,
Action<string>
>;
const _reducerBuilder = <State, TheAction extends Action<string>>(
cases: EasyToWorkWithReducerCase<State>[],
middlewareCallbacks: ((state: State, action: TheAction) => void)[] = [],
): ReducerBuilder<State, TheAction> => {
const reducer = (state: State, action: TheAction): State => {
const correspondingCase = cases.find(({ type }) => type === action.type);
if (!correspondingCase)
throw new Error(`received unknown action type "${action.type}"`);
middlewareCallbacks.forEach((callback) => {
callback(state, action);
});
return correspondingCase.handler(state, action);
};
const warnAboutPrematureMiddleware = () => {
console.warn(
"You have added a case to a reducer after adding middleware. It is recommended that middleware is added last.",
);
};
const addCase = <NewActionName extends string>(
name: NewActionName,
handler: (state: State, action: Action<NewActionName>) => State,
): ReducerBuilder<State, TheAction | Action<NewActionName>> => {
if (middlewareCallbacks.length > 0) warnAboutPrematureMiddleware();
return _reducerBuilder([
...cases,
{
type: name,
handler: handler as any,
},
]);
};
const addPayloadCase =
<NewActionName extends string>(name: NewActionName) =>
<Payload>(
handler: (
state: State,
action: PayloadAction<NewActionName, Payload>,
) => State,
): ReducerBuilder<
State,
TheAction | PayloadAction<NewActionName, Payload>
> => {
if (middlewareCallbacks.length > 0) warnAboutPrematureMiddleware();
return _reducerBuilder([
...cases,
{
type: name,
handler: handler as any,
},
]);
};
const addMiddleware: ReducerBuilder<State, TheAction>["addMiddleware"] = (
middlewareCallback,
) => _reducerBuilder(cases, [...middlewareCallbacks, middlewareCallback]);
return {
addCase,
addPayloadCase,
build: () => reducer,
addMiddleware,
};
};
export const reducerBuilder = <State>() => _reducerBuilder<State, never>([]);
export const middlewareForActionType =
<
State,
TheAction extends Action<string>,
AllowedTypes extends TheAction["type"],
>(
types: AllowedTypes[],
callback: (
state: State,
action: Extract<TheAction, { type: AllowedTypes }>,
) => void,
) =>
(state: State, action: TheAction) => {
if (types.includes(action.type as AllowedTypes)) {
callback(state, action as any);
}
};
export const middlewareForAllActionsExcept =
<
State,
TheAction extends Action<string>,
DisallowedTypes extends TheAction["type"],
>(
types: DisallowedTypes[],
callback: (
state: State,
action: Exclude<TheAction, { type: DisallowedTypes }>,
) => void,
) =>
(state: State, action: TheAction) => {
if (!types.includes(action.type as DisallowedTypes)) {
callback(state, action as any);
}
};
type CounterState = {
value: number;
};
const counterReducer = reducerBuilder<CounterState>()
.addCase("increment", (state) => ({
value: state.value + 1,
}))
.addPayloadCase("add")<number>((state, { payload }) => ({
value: state.value + payload,
}))
.addMiddleware(
middlewareForActionType(["add", "increment"], (state, action) => {}),
)
.addMiddleware(middlewareForAllActionsExcept(["add"], (state, action) => {}))
.addMiddleware(
middlewareForAllActionsExcept(["increment"], (state, action) => {}),
)
.build();
counterReducer(
{
value: 0,
},
{
type: "add",
payload: 5,
},
);
import { MouseEvent, MouseEventHandler } from "react";
type Coordinates = {
x: number;
y: number;
};
const deepCopy = <T>(data: T): T => JSON.parse(JSON.stringify(data));
export const fireClickRipple = (clickEvent: MouseEvent) => {
const element = clickEvent.currentTarget as HTMLElement;
const rect = element.getBoundingClientRect();
const cursorCoordinates: Coordinates = {
x: clickEvent.clientX,
y: clickEvent.clientY,
};
const elementCoordinates: Coordinates = {
x: rect.left,
y: rect.top,
};
const coordinatesRelativeToElement = {
x: cursorCoordinates.x - elementCoordinates.x,
y: cursorCoordinates.y - elementCoordinates.y,
};
const currentPositionValue = window.getComputedStyle(element).position;
const originalElementStyle = deepCopy(element.style);
if (currentPositionValue === "static" || currentPositionValue === "") {
// If the element is static, we need to set it to relative
element.style.position = "relative";
}
element.style.overflow = "hidden";
const rippleDiv = document.createElement("div");
rippleDiv.style.position = "absolute";
const dimensionMultiplier = 2;
const width = dimensionMultiplier * rect.width;
const height = dimensionMultiplier * rect.height;
const finalDimension = Math.max(height, width);
// rippleDiv.style.left = `${coordinatesRelativeToElement.x - width / 2}px`;
rippleDiv.style.left = `${
coordinatesRelativeToElement.x - finalDimension / 2
}px`;
rippleDiv.style.top = `${
coordinatesRelativeToElement.y - finalDimension / 2
}px`;
rippleDiv.style.width = `${finalDimension}px`;
rippleDiv.style.height = `${finalDimension}px`;
rippleDiv.style.borderRadius = "50%";
rippleDiv.style.background = "black";
element.appendChild(rippleDiv);
const duration = 500;
rippleDiv.animate(
[
{
transform: "scale(0) ",
opacity: 0.5,
},
{
transform: "scale(1)",
offset: 0.7,
},
{
opacity: 0,
offset: 1,
},
],
{
duration,
easing: "linear",
},
);
setTimeout(() => {
rippleDiv.remove();
element.style.overflow = originalElementStyle.overflow;
element.style.position = originalElementStyle.position;
}, duration);
};
export const withRipple =
(extraHandler: MouseEventHandler | undefined): MouseEventHandler =>
(event) => {
fireClickRipple(event);
extraHandler?.(event);
};
/* eslint-disable @typescript-eslint/naming-convention */
type INTERNAL_Opaque<BaseType, Token extends string> = BaseType & {
____________________: Token;
};
type INTERNAL_PseudoSymbol<Token extends string> = INTERNAL_Opaque<
Token,
Token
>;
export type Page = INTERNAL_PseudoSymbol<'page'>;
// export type Route = RouteEnd | Record<string, Route>;
type SAMPLE_RouteMap = {
// $ `Page` means its a valid route
about: Page;
// $ `Page & {...}` means its a valid route, and it has
// $ children
posts: Page & {
'[slug]': Page;
new: Page;
};
// $ `auth` does not use `Page`, meaning that `/auth` on
// $ its own is not a valid route.
auth: {
// $ `auth` children use `Page`, so while `/auth` is
// $ not a valid route, `/auth/signIn` and
// $ `/auth/signUp` are valid routes
signIn: Page;
signUp: Page;
};
profile: Page & {
'[slug]': Page & {
edit: Page;
};
};
};
// What a route path type generated from the above type should look like
type SAMPLE_RouteMap_Result =
| '/'
| '/about'
| '/posts'
| '/posts/[slug]'
| '/posts/new'
| '/auth/signIn'
| '/auth/signUp'
| '/profile'
| '/profile/[slug]'
| '/profile/[slug]/edit';
type IsPage<T> = T extends Page ? true : false;
type A = IsPage<SAMPLE_RouteMap['about']>
type B = IsPage<SAMPLE_RouteMap['posts']>
type C = IsPage<SAMPLE_RouteMap['auth']>
/* eslint-disable no-underscore-dangle */
type Comparator<T> = (a: T, b: T) => boolean;
export const datesAreEqual = (a: Date, b: Date): boolean =>
a.getTime() === b.getTime();
const _setsAreEqual = <T>(
a: Set<T>,
b: Set<T>,
itemsAreEqual: Comparator<T>,
): boolean => {
if (a.size !== b.size) return false;
const aValues = Array.from(a);
const bValues = Array.from(b);
const aContainsAllBValues = bValues.every((bValue) =>
aValues.some((aValue) => itemsAreEqual(aValue, bValue)),
);
if (!aContainsAllBValues) return false;
const bContainsAllAValues = aValues.every((aValue) =>
bValues.some((bValue) => itemsAreEqual(aValue, bValue)),
);
return bContainsAllAValues;
};
const _mapsAreEqual = <T>(
a: Map<unknown, T>,
b: Map<unknown, T>,
itemsAreEqual: Comparator<T>,
) => {
if (a.size !== b.size) return false;
return Array.from(a.entries()).every(([key, value]) => {
if (!b.has(key)) return false;
return itemsAreEqual(value, b.get(key) as T);
});
};
const _arraysAreEqual = <T>(a: T[], b: T[], itemsAreEqual: Comparator<T>) => {
if (a.length !== b.length) return false;
return a.every((aItem, index) => itemsAreEqual(aItem, b[index] as T));
};
/**
* IMPORTANT: I have not actually tested this code, and even if it
* does work I'm certain I'm missing out on some cases where its possible
* to perform a generalised deep equality check.
*
* There are some situations where a "generalised" deep equality check
* is not possible. Eg; in a WeakSet or WeakMap, its not possible to
* just iterate over all values, instead values can only be accessed
* by their keys, so the only way to check their equality would be to
* know all the keys you need to check.
*/
export const isEqualDeep = (a: unknown, b: unknown): boolean => {
if (a === b) return true;
if (typeof a !== typeof b) return false;
if (Number.isNaN(a) && Number.isNaN(b)) return true;
if (a instanceof Date && b instanceof Date) return datesAreEqual(a, b);
if (Array.isArray(a) && Array.isArray(b)) {
return _arraysAreEqual(a, b, isEqualDeep);
}
if (a instanceof Set && b instanceof Set)
return _setsAreEqual(a, b, isEqualDeep);
if (a instanceof Map && b instanceof Map)
return _mapsAreEqual(a, b, isEqualDeep);
if (a === null || b === null) return true;
if (typeof a === "object" && typeof b === "object") {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) return false;
return aKeys.every((key) =>
isEqualDeep(a[key as keyof typeof a], b[key as keyof typeof b]),
);
}
console.warn(
"it looks like you have tried to perform a deep quality check on an unsupportd data type. The result will always be false. You will need to work out a custom equality check for this data type.",
);
return false;
};
export const setsAreEqual = <T>(
a: Set<T>,
b: Set<T>,
itemsAreEqual: Comparator<T> = isEqualDeep,
): boolean => _setsAreEqual(a, b, itemsAreEqual);
export const mapsAreEqual = <T>(
a: Map<unknown, T>,
b: Map<unknown, T>,
itemsAreEqual: Comparator<T> = isEqualDeep,
): boolean => _mapsAreEqual(a, b, itemsAreEqual);
export const arraysAreEqual = <T>(
a: T[],
b: T[],
itemsAreEqual: Comparator<T> = isEqualDeep,
): boolean => _arraysAreEqual(a, b, itemsAreEqual);
const expectParam =
<Param>() =>
(param: Param) =>
param;
export default expectParam;
const hasStringMessage = (val: unknown): val is Record<'message', string> =>
typeof val === 'object' &&
val !== null &&
'message' in val &&
typeof val.message === 'string';
export const extractErrorMessage = (
errorObject: unknown,
fallback?: string
): string => {
if (typeof errorObject === 'string') return errorObject;
if (hasStringMessage(errorObject)) return errorObject.message;
if (fallback !== undefined) return fallback;
// Last resort, if we are unable to reasonably derive
// an error message from the object, we just stringify
// the whole thing and return that
return JSON.stringify(errorObject);
};
export const findDeepInObject = (
obj: Record<string, any>,
predicate: (key: string, value: unknown) => boolean,
): unknown => {
for (const key in obj) {
if (predicate(key, obj[key])) return obj[key];
if (typeof obj[key] === "object") {
const result = findDeepInObject(obj[key], predicate);
if (result) return result;
}
}
return undefined;
};
const hasStringMessage = (val: unknown): val is Record<"message", string> =>
typeof val === "object" &&
val !== null &&
"message" in val &&
typeof val.message === "string";
/**
* Searches deeply in an object for a message string.
*/
export const extractErrorMessage = (
errorObject: unknown,
fallback?: string,
): string => {
if (typeof errorObject === "string") return errorObject;
if (hasStringMessage(errorObject)) return errorObject.message;
if (typeof errorObject === "object" && errorObject !== null) {
// Iteratively search through an object to find a message
let message: string | undefined;
findDeepInObject(errorObject, (key, value) => {
if (hasStringMessage(value)) {
message = value.message;
return true;
}
if (key === "message" && typeof value === "string") {
message = value;
return true;
}
return false;
});
if (message !== undefined) return message;
}
if (fallback !== undefined) return fallback;
// Last resort, if we are unable to reasonably derive
// an error message from the object, we just stringify
// the whole thing and return that
return JSON.stringify(errorObject, null, 2);
};
const NONE = Symbol('none');
/**We use a custom symbol instead of just null or undefined, since
* its possible that the best item in an array to be null or undefined.
*/
export function findBest<T>(
arr: T[],
scorer: (a: T) => number, // score of <= 0 is not counted as a match
): T | undefined {
let best: T | typeof NONE = NONE;
let bestScore = 0;
arr.forEach((item) => {
const score = scorer(item);
if (score > bestScore) {
best = item;
bestScore = score;
}
});
if (best === NONE) {
return undefined;
}
return best;
}
/**
* A "safe" version of the array "find" method that
* throws an error if no value is found. This means
* that the return value of the function is guaranteed
* and does not need to be checked for null or
* undefined.
*
* @param arr The array to search
* @param predicate The function used to find the
* value
* @param [errorMessage] The message to include in the
* error that is thrown if no value is found. Defaults
* to "not found"
* @returns The first value in the array that returns
* `true` when passed to the predicate function. If
* no item in the array returns `true` from the
* predicate, an error is thrown.
*/
const findOrThrow = <T>(arr: T[], predicate: (p: T) => boolean): T => {
const result = arr.find(predicate);
if (!result) throw Error("Item not found");
return result;
};
export const _findOrThrow =
<T>(predicate: (p: T) => boolean, errorMessage: string = "not found") =>
(arr: T[]) =>
findOrThrow(arr, predicate, errorMessage);
export default findOrThrow;
/**
* USAGE: import this file at the top of your app's root. From then on any calls to
* `JSON.parse` that are not passed a custom reviver will fix any dates that were
* stringified by JSON.stringify.
*/
const isoDatePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.Z:+\-\d]*?$/;
const dotnetDatePattern = /^\/Date\((\d+)([+\-\d]*)\)\/$/;
const baseJSONParse = JSON.parse;
JSON.parse = (text, customReviver) =>
baseJSONParse(text, (key, value) => {
if (customReviver) return customReviver(key, value);
if (typeof value === "string" && isoDatePattern.test(value))
return new Date(value);
const dotnetResult = dotnetDatePattern.exec(value);
if (dotnetResult)
return new Date(
dotnetResult.slice(1, 3).reduce((a, x) => a + (parseInt(x) || 0), 0),
);
return value;
});
if (window?.Response)
Response.prototype.json = async function () {
return JSON.parse(await this.text());
};
/**
* Take an array of strings and join them so that they can
* be used in a `font-family` css style declaration.
*
* The idea is to provide a more declarative and composable
* way to apply font family styles.
*/
const fontFamilyRule = (families: string[]): string => {
const formatted = families.map((familyName) =>
familyName.includes(" ") ? `"${familyName}"` : familyName,
);
const joined = formatted.join(", ");
return joined;
};
export default fontFamilyRule;
const getMaxIndex = (arr: unknown[]) => arr.length - 1;
const forEachBackAndForth = <Element>(
arr: Element[],
iterator: (value: Element, index: number, array: Element[]) => void,
): void => {
if (arr.length === 0) return;
const iterations = getMaxIndex(arr) * 2;
for (let i = 0; i <= iterations; i += 1) {
const index = i > getMaxIndex(arr) ? iterations - i : i;
iterator(arr[index], index, arr);
}
};
export default forEachBackAndForth;
const generateArray = <Element>(
length: number,
generator: (i: number) => Element,
): Element[] => [...Array(length)].map((_, index) => generator(index));
export default generateArray;
// NOTE: My original implementation of this uses lodash functions,
// they should be easy to replace with your own stuff.
/**
* Generates an array of random percentage decimals that sum
* to 1. Will provide decent variety of percentages.
*
* @param {number} numberOfValues
* @returns {number[]}
*/
const generatePercentagesThatSumToOne = (numberOfValues) => {
if (numberOfValues <= 0) {
return [];
}
/**
* @type number[]
*/
const percentages = [];
const getCurrentSumOfPercentages = () => sum(percentages);
for (let i = 0; i < numberOfValues - 1; i++) {
// We only iterate up to the second to last value, then
// the last value will be equal to whatever space is left.
// We dynamically change the upper and lower bounds of the
// percentage share generation to make sure that we can have
// a good variety of percentages and make sure that we don't
// go over 1 before the last percentage share is generated.
const spaceLeft = 1 - getCurrentSumOfPercentages();
const numberOfIndexesLeft = numberOfValues - i;
const shareIfRestOfSharesWereDividedEqually =
spaceLeft / numberOfIndexesLeft;
const randomShareMin = shareIfRestOfSharesWereDividedEqually / 2;
const randomShareMax = shareIfRestOfSharesWereDividedEqually * 2;
const share = generateNumberBetween(randomShareMin, randomShareMax);
const roundedShare = round(share, 2);
percentages.push(roundedShare);
}
const sumOfAllCurrentShares = getCurrentSumOfPercentages();
if (sumOfAllCurrentShares >= 1)
return generatePercentagesThatSumToOne(numberOfValues);
// In case we have mistakenly already added up to more than 1,
// we just retry
const remainingSpace = 1 - sumOfAllCurrentShares;
percentages.push(round(remainingSpace, 2));
return percentages;
};
/**
* @param {number} numberOfValues
* @returns {number[]}
*/
const generateTwoDecimalPercentagesThatSumToOne = (numberOfValues) => {
// You can expand this to take the decimal places as a parameter
// by using Math.pw on 10
if (numberOfValues <= 0) {
return [];
}
/**
* @type number[]
*/
const percentagesAsInts = [];
const getCurrentSumOfPercentagesAsInts = () => sum(percentagesAsInts);
for (let i = 0; i < numberOfValues - 1; i++) {
// We dynamically change the upper and lower bounds of the
// percentage share generation to make sure that we can have
// a good variety of perctages and make sure that we don't
// go over 1 before the last percentage share is generated.
const spaceLeft = 100 - getCurrentSumOfPercentagesAsInts();
const numberOfIndexesLeft = numberOfValues - i;
const shareIfRestOfSharesWereDividedEqually = round(
spaceLeft / numberOfIndexesLeft,
0,
);
const randomShareMin = round(shareIfRestOfSharesWereDividedEqually / 2, 0);
const randomShareMax = shareIfRestOfSharesWereDividedEqually * 2;
const share = generateNumberBetween(randomShareMin, randomShareMax, false);
percentagesAsInts.push(share);
}
const sumOfAllCurrentIntPercentages = getCurrentSumOfPercentagesAsInts();
if (sumOfAllCurrentIntPercentages >= 100)
return generatePercentagesThatSumToOne(numberOfValues);
// In case we have mistakenly already added up to more than 1,
// we just retry
const remainingSpaceAsInt = 100 - sumOfAllCurrentIntPercentages;
percentagesAsInts.push(round(remainingSpaceAsInt, 2));
const actualPercentages = percentagesAsInts.map((percentageAsInt) =>
// We do this string trick because JS math can
// produce weird results when working with floats
Number(`0.${percentageAsInt}`),
);
return actualPercentages;
};
import { F } from "@mobily/ts-belt";
export type ArrayDifferenceType = "added" | "removed" | "updated";
export type ArrayDifference<T> = {
type: ArrayDifferenceType;
data: T;
};
type BatchedArrayDifferences<T> = {
type: ArrayDifferenceType;
data: T[];
};
/**
* @param baseArr
* @param newArr
* @param idDeriver
*/
export const getArrayDifference = <T>(
baseArr: T[],
newArr: T[],
idDeriver: (item: T) => string | number
) => {
const addedItems: BatchedArrayDifferences<T> = {
type: "added",
data: newArr.filter(
(item) => !baseArr.some((i) => idDeriver(i) === idDeriver(item))
),
};
const removedItems: BatchedArrayDifferences<T> = {
type: "removed",
data: baseArr.filter(
(item) => !newArr.some((i) => idDeriver(i) === idDeriver(item))
),
};
const changedItems: BatchedArrayDifferences<T> = {
type: "updated",
data: newArr.filter((item) => {
const equivalentFromBase = baseArr.find(
(baseItem) => idDeriver(baseItem) === idDeriver(item)
);
if (!equivalentFromBase) return false;
return !F.equals(equivalentFromBase, item);
}),
};
const differences: ArrayDifference<T>[] = [
addedItems,
removedItems,
changedItems,
]
.map((differenceSet) =>
differenceSet.data.map((differenceEntity) => ({
type: differenceSet.type,
data: differenceEntity,
}))
)
.flat();
return differences;
};
export type GetStringDiffScoreOptions = {
caseSensitive: boolean;
};
// Uses levenshtein distance to calculate a numerical
// score indicating how different 2 strings are
//
// CREDIT: This code was largely generated by Copilot,
// although I did clean it up a bit
const getStringDiffScore = (
a: string,
b: string,
{ caseSensitive = true }: Partial<GetStringDiffScoreOptions> = {},
): number => {
const finalA = caseSensitive ? a : a.toLowerCase();
const finalB = caseSensitive ? b : b.toLowerCase();
// If one of strings is empty, the difference score
// is just the length of the other string
if (!finalA.length) return finalB.length;
if (!finalB.length) return finalA.length;
const matrix: number[][] = [];
// increment along the first column of each row
for (let row = 0; row <= finalB.length; row += 1) {
matrix[row] = [row];
}
// increment each column in the first row
for (let col = 0; col <= finalA.length; col += 1) {
matrix[0][col] = col;
}
// Fill in the rest of the matrix
for (let row = 1; row <= finalB.length; row += 1) {
for (let col = 1; col <= finalA.length; col += 1) {
if (finalB.charAt(row - 1) === finalA.charAt(col - 1)) {
matrix[row][col] = matrix[row - 1][col - 1];
} else {
matrix[row][col] = Math.min(
matrix[row - 1][col - 1] + 1, // substitution
Math.min(
matrix[row][col - 1] + 1, // insertion
matrix[row - 1][col] + 1,
),
); // deletion
}
}
}
return matrix[finalB.length][finalA.length];
};
export default getStringDiffScore;

Good Libraries

  • Masonry Layout (React Only) - Masonic
const hasCollision = <T>(arr: T[], collisionChecker: (a: T, b: T) => boolean) =>
arr.some((el, index, baseArr) => {
const itemsToCompare = baseArr.slice(index + 1);
return itemsToCompare.some((otherEl) => collisionChecker(el, otherEl));
});
export default hasCollision;

Add the following to your package.json scripts:

{
	"check-types": "tsc --noEmit"
}
export const invertTypeGuard =
<A>(typeGuard: (value: unknown) => value is A) =>
<B>(value: A | B): value is B =>
!typeGuard(value);
// Example Usage
const isNull = (value: unknown): value is null => value === null;
const numberAndNullArray = [1, 2, null, 4];
const nullOnly = numberAndNullArray.filter(isNull);
const numberOnly = numberAndNullArray.filter(invertTypeGuard(isNull));
/**
* Check if a key exists in an object. This works
* identically to the "in" keyword built in to JS,
* but also works as a typeguard to provide type
* information to typescript, which the "in" keyword
* does not do (at least in the current version of
* typescript, 4.6.4)
*
* @param key - The key to check for
* @param obj - The object within which to look for
* the key
* @returns Whether or not the key exists in 'obj'.
* In typescript, if the function returns true, then
* 'obj' is typed as `Record<Key, unknown>`, with
* `Key` being the key that was checked for.
*/
const keyIsInObject = <Key extends string>(
key: Key,
obj: any,
): obj is Record<Key, unknown> => key in obj;
export default keyIsInObject;
/* #region UTILITY TYPES */
/**
* You may already have equivalents of these types in your project. If not,
* copy these into a common utility types file and export them.
*
* Both of these types can be found verbatim in the `type-fest` npm package,
* unless the names have changed since writing this.
*/
type ValueOf<T> = T[keyof T]; // Can be found verbatim in
type TaggedUnion<
TagKey extends string,
TagMap extends Record<string, Record<string, unknown>>,
> = ValueOf<{
[SpecificTag in keyof TagMap]: TagMap[SpecificTag] & {
[TheTagKey in TagKey]: SpecificTag;
};
}>;
/* #endregion */
const NO_PAYLOAD_SYMBOL = Symbol("NO_PAYLOAD");
export type NoPayload = typeof NO_PAYLOAD_SYMBOL;
export type DefineActions<ActionRegistry extends Record<string, unknown>> =
TaggedUnion<
"type",
{
[ActionType in keyof ActionRegistry]: ActionRegistry[ActionType] extends NoPayload
? {}
: {
payload: ActionRegistry[ActionType];
};
}
>;
// # Example Usage
type CounterAction = DefineActions<{
increment: NoPayload;
decrement: NoPayload;
set: number;
clamp: {
min: number;
max: number;
};
}>;
const increment: CounterAction = {
type: "increment",
};
const decrement: CounterAction = {
type: "decrement",
};
const set: CounterAction = {
type: "set",
payload: 5,
};
const clamp: CounterAction = {
type: "clamp",
payload: {
min: 0,
max: 10,
},
};
// @ts-expect-error
const invalidSet: CounterAction = {
type: "set",
};
type ConsoleLevel = 'warning' | 'error' | 'info' | 'debug';
type PendingConsoleLog = {
params: any[];
level: ConsoleLevel;
firedAt: Date;
};
type CustomConsoleConstructorOptions = {
defaultLevel: ConsoleLevel;
labels: string[];
collectingTimeout: number;
};
type CustomConsoleExtensionNewLabelHandlingMode = 'append' | 'prepend' | 'override';
const DEFAULT_NEW_LABEL_HANDLING_MODE: CustomConsoleExtensionNewLabelHandlingMode = 'append';
type CustomConsoleExtensionOptions = CustomConsoleConstructorOptions & {
/**
* How should new labels interact with the labels already
* present in the console being extended?
* - 'append' (default) new labels are added to the end of the list
* - 'prepend' new labels are added to the beginning of the list
* - 'override' new labels replace the existing labels
*/
handleNewLabels: CustomConsoleExtensionNewLabelHandlingMode;
/**
* Function to modify the labels before they are set in the new
* console. Allows you to customise the labels of the base console.
*/
modifyLabels: (labels: string[]) => string[];
};
const defaultCustomConsoleOptions: CustomConsoleConstructorOptions = {
labels: [],
defaultLevel: 'info',
collectingTimeout: 3000
};
const defaultExtensionOptions: Omit<
CustomConsoleExtensionOptions,
keyof CustomConsoleConstructorOptions
> = {
handleNewLabels: DEFAULT_NEW_LABEL_HANDLING_MODE,
modifyLabels: (labels) => labels
};
const composeLabelPrefix = (labels: string[]) => {
const combinedLabels = labels.join(', ');
const prefix = labels.length ? `(${combinedLabels})` : '';
return prefix;
};
const composeLogParams = (labels: string[], messages: any[]): [string, ...any[]] => {
const prefix = composeLabelPrefix(labels);
return [`${prefix}`, ...messages];
};
/**
* TODO: Integrate functionality of my "progressLogger" into this
*/
/**
* Uses effectively the same API as the native console, but with some added
* features:
* - Create new instances of the console with custom default
* settings
* - Extend an instance of the console with settings
* - Define labels that are prefixed to each log message
* - Set the default console level used by the `log` method
* - "Collecting" mode which debounces logs and then logs them
* all at once later
*/
class CustomConsole {
labels: string[];
defaultLevel: ConsoleLevel;
private pendingLogs: PendingConsoleLog[] = [];
collectingTimeoutDuration: number;
// We need the `isCollecting` field to be writable internally,
// but readonly from the outside. We achieve this by storing
// the value in a private field and exposing it through a
// getter.
private _isCollecting = false;
private groupLabel: string | undefined = undefined;
disabled = false;
private collectingTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
constructor(userOptions: Partial<CustomConsoleConstructorOptions> = {}) {
const options: CustomConsoleConstructorOptions = {
...defaultCustomConsoleOptions,
...userOptions
};
this.labels = options.labels;
this.defaultLevel = options.defaultLevel;
this.collectingTimeoutDuration = options.collectingTimeout;
}
private composeNewLabels(
newLabels: string[],
mode: CustomConsoleExtensionNewLabelHandlingMode
): string[] {
switch (mode) {
case 'prepend':
return [...newLabels, ...this.labels];
case 'override':
return newLabels;
default:
// 'append'
return [...this.labels, ...newLabels];
}
}
get isCollecting() {
return this._isCollecting;
}
extend(extraOptions: Partial<CustomConsoleExtensionOptions> = {}): CustomConsole {
const currentOptions: CustomConsoleConstructorOptions = {
defaultLevel: this.defaultLevel,
labels: this.labels,
collectingTimeout: this.collectingTimeoutDuration
};
const extensionOptions: CustomConsoleExtensionOptions = {
...currentOptions,
...defaultExtensionOptions,
...extraOptions
};
const withExtraLabels = this.composeNewLabels(
extensionOptions.labels,
extensionOptions.handleNewLabels
);
const newLabels = extensionOptions.modifyLabels(withExtraLabels);
const newConsole = new CustomConsole({
labels: newLabels,
defaultLevel: extensionOptions.defaultLevel
});
return newConsole;
}
private clearCollectingTimeout() {
if (this.collectingTimeout) {
clearTimeout(this.collectingTimeout);
}
}
private startCollectingTimeout() {
this.collectingTimeout = setTimeout(
() => this.endCollecting(),
this.collectingTimeoutDuration
);
}
private resetCollectingTimeoutCountdown() {
this.clearCollectingTimeout();
this.startCollectingTimeout();
}
logToLevel(level: ConsoleLevel, ...logParams: any[]) {
if (this.disabled) return undefined;
const finalLogParams = composeLogParams(this.labels, logParams);
if (!this._isCollecting) {
switch (level) {
case 'warning':
console.warn(...finalLogParams);
break;
case 'error':
console.error(...finalLogParams);
break;
case 'info':
console.info(...finalLogParams);
break;
case 'debug':
console.debug(...finalLogParams);
break;
default:
console.log(...finalLogParams);
break;
}
} else {
// If it is collecting...
this.pendingLogs.push({
level,
params: logParams,
firedAt: new Date()
});
this.resetCollectingTimeoutCountdown();
}
}
clearPendingLogs() {
this.pendingLogs = [];
}
private executePendingLogs() {
const prefix = composeLabelPrefix(this.labels);
const prefixes = [prefix, this.groupLabel].filter(
(label) => typeof label === 'string'
) as string[];
console.group(prefixes.join(' '));
this.pendingLogs.forEach(({ level, params }) => {
this.logToLevel(level, ...params);
});
console.groupEnd();
this.clearPendingLogs();
}
/**
* TODO: Change this input to be an object
*/
startCollecting(groupLabel?: string, timeoutDuration = this.collectingTimeoutDuration) {
this.collectingTimeoutDuration = timeoutDuration;
// We don't actually start the timeout to run the logs here,
// instead that is done in the `logToLevel` method, that way
// we aren't starting the countdown until the first log is
// actually made.
this._isCollecting = true;
this.groupLabel = groupLabel;
}
endCollecting() {
this._isCollecting = false;
this.executePendingLogs();
}
log(...params: any[]) {
this.logToLevel(this.defaultLevel, ...params);
}
/**
* Methods for each level, same as the native console
*/
info(...params: any[]) {
this.logToLevel('info', ...params);
}
debug(...params: any[]) {
this.logToLevel('debug', ...params);
}
warning(...params: any[]) {
this.logToLevel('warning', ...params);
}
error(...params: any[]) {
this.logToLevel('error', ...params);
}
}
export default CustomConsole;
/* ---------------------------------- */
/* Types */
/* ---------------------------------- */
/* ---------- Utility Types --------- */
type AnyDict = Record<string, any>;
type ValueOf<T extends AnyDict | any[]> = T[keyof T];
// This should probably be moved into a `utilityTypes`
// file
type DeepPartial<T> = T extends AnyDict
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
// This should probably be moved into a `utilityTypes`
// file
/* ----------- Merge Types ---------- */
type ShallowMergeRecords<A extends AnyDict, B extends AnyDict> = B &
Omit<A, keyof B>;
type ShallowMerge<A, B> = A extends AnyDict
? B extends AnyDict
? ShallowMergeRecords<A, B>
: B
: B;
// Take the fields from two object types that are not shared
// between them
type ShallowDiff<A extends AnyDict, B extends AnyDict> = Omit<A, keyof B> &
Omit<B, keyof A>;
// Take the fields from two object types that are shared
// between them
export type ShallowOverlap<T extends AnyDict, U extends AnyDict> = Omit<
T | U,
keyof ShallowDiff<T, U>
>;
export type DeepMerge<T extends AnyDict, U extends AnyDict> = ShallowDiff<
T,
U
> & {
[OverlappingKey in keyof ShallowOverlap<T, U>]: [
T[OverlappingKey],
U[OverlappingKey],
] extends [AnyDict, AnyDict]
? ValueOf<T[OverlappingKey]> | ValueOf<U[OverlappingKey]> extends AnyDict // If either of the objects contain nested objects, // perform a deep merge
? DeepMerge<T[OverlappingKey], U[OverlappingKey]> // If neither of the objects have nested objects, // do a shallow merge
: ShallowMerge<T[OverlappingKey], U[OverlappingKey]>
: U;
};
/* ---------------------------------- */
/* Main Code */
/* ---------------------------------- */
/* -------- Utility Functions ------- */
const isObjectLike = (item: any) =>
typeof item === "object" && item !== null && !(item instanceof Date);
const isPlainObject = (item: any): item is Record<string, any> =>
isObjectLike(item) && !Array.isArray(item);
const isDate = (item: unknown): item is Date => item instanceof Date;
const copyDate = (date: Date): Date => new Date(date.getTime());
// Creates a copy of an object by recreating all
// values within it, must be done to avoid mutating
// during the merge
// NOTE: Can be replaced with built in `structuredClone`
const deepCopy = <T>(item: T): T => {
// If the object is primitive we just give it back
if (isDate(item)) {
return copyDate(item) as any as T;
}
if (!isObjectLike(item)) {
return item;
}
const result: AnyDict = {};
Object.entries(item as AnyDict).forEach(([key, value]) => {
if (isPlainObject(value)) {
result[key] = deepCopy(value);
} else {
result[key] = value;
}
});
return result as T;
};
/* ----------- Merge Code ----------- */
// NOTE: If your project already includes a merge
// function (eg; lodash's merge) you can use that
// as the untyped merge and adapt the `deepMerge`
// function to use that.
// Performs the actual merge, the 'deepMerge' function
// just runs this function and injects correct typing
// Current version not work on Sets and Maps
const untypedMerge = (source: AnyDict, update: AnyDict): AnyDict => {
// Use the source as the starting point for the result
// We run source through `deepCopy` to prevent mutating it
const result: AnyDict = deepCopy(source);
Object.entries(update).forEach(([key, valueFromUpdate]) => {
const valueFromSource = source[key];
const bothValuesArePlainObjects = [valueFromSource, valueFromUpdate].every(
isPlainObject,
);
if (bothValuesArePlainObjects) {
// Merge the values if they are both objects
result[key] = untypedMerge(valueFromSource, valueFromUpdate);
} else {
// If only 1 or neither of them are objects, replace
// with value from update
result[key] = valueFromUpdate;
}
});
return result;
};
// We have to use function keyword cus otherwise TS gives
// an error about having to infer the type of the variable.
// I guess we get around by declaring it with the `function`
// keyword
function deepMerge<Source extends AnyDict, Update extends AnyDict>(
source: Source,
update: Update,
): DeepMerge<Source, Update> {
return untypedMerge(source, update) as DeepMerge<Source, Update>;
}
// This functions identically to the `deepMerge` function,
// but with the type of the second parameter being a deep
// partial version of the first, and the return type
// being the type of the first param. This is useful for
// providing an 'update' of an object without changing it's
// type, eg; in a redux reducer.
export function deepUpdate<Source extends AnyDict>(
source: Source,
update: DeepPartial<Source>,
) {
return deepMerge(source, update) as any as Source;
}
// NOTE: When importing this into a project, try out using
// an arrow function for the `deepMerge` functions. The
// `function` keyword is used here because that was the
// only way to get it to work when this was initially written
// in a `tsdx` template project.
export default deepMerge;
import { Action, ActionOn, Computed, ThunkOn } from "easy-peasy";
import { EmptyObject } from "../../utils/utilityTypes";
type StateActionOrComputed<Model extends object, OtherField> =
| Action<Model, OtherField>
| Computed<Model, OtherField>;
type StateListeners<Model extends object> = ActionOn<Model> | ThunkOn<Model>;
type AnySliceActions<Model extends object> = Record<
string,
StateActionOrComputed<Model, any>
>;
type AnySliceListeners<Model extends object> = Record<
string,
StateListeners<Model>
>;
export const createSlice = <
Model extends object,
Extras extends AnySliceActions<Model>,
Listeners extends
| AnySliceListeners<Model & Extras>
| EmptyObject = EmptyObject,
>(
initialState: Model,
extras: Extras,
listeners: Listeners = {} as Listeners,
) => ({
...initialState,
...extras,
...listeners,
});
/**
* Usage:
*
* // todoSlice.ts
* const todosSlice = createSlice(...);
*
* // store.ts
* const store = createStore({
* todos: todosSlice,
* })
*/
import { nanoid as generateId } from "nanoid";
type SubscriberCallback<EventPayload> = (
payload: EventPayload,
eventId: string,
) => void | Promise<void>;
export type Subscriber<EventPayload> = {
callback: SubscriberCallback<EventPayload>;
id: string;
};
const composeSubscriber = <EventPayload>(
callback: SubscriberCallback<EventPayload>,
): Subscriber<EventPayload> => ({
callback,
id: generateId(),
});
class Emitter<EventPayload> {
private subscribers: Subscriber<EventPayload>[] = [];
constructor() {
this.subscribers = [];
}
private unsubscribe = (id: string): void => {
this.subscribers = this.subscribers.filter(
(subscriber) => subscriber.id !== id,
);
};
subscribe = (callback: SubscriberCallback<EventPayload>): (() => void) => {
const newSubscriber = composeSubscriber(callback);
this.subscribers.push(newSubscriber);
return () => this.unsubscribe(newSubscriber.id);
};
emit = (payload: EventPayload, eventId = generateId()): void => {
this.subscribers.forEach(({ callback }) => callback(payload, eventId));
};
}
export default Emitter;
type WithId = {
id: string;
};
type Array<T> = T[] | readonly T[];
/* ----- Filter Out Item With ID ---- */
export const filterOutItemWithId = <T extends WithId>(
arr: Array<T>,
id: string,
): Array<T> => arr.filter((item) => item.id !== id);
export const _filterOutItemWithId =
<T extends WithId>(id: string) =>
(arr: Array<T>): Array<T> =>
filterOutItemWithId(arr, id);
/* ------- Update Item With Id ------ */
export const updateItemWithId = <T extends WithId>(
arr: Array<T>,
id: string,
update: (p: T) => T,
): Array<T> =>
arr.map((item) => {
if (item.id !== id) {
return item;
}
return update(item);
});
export const _updateItemWithId =
<T extends WithId>(id: string, update: (p: T) => T) =>
(arr: Array<T>): Array<T> =>
updateItemWithId(arr, id, update);
/* -------- Find Item With Id ------- */
export const findItemWithId = <T extends WithId>(
arr: Array<T>,
id: string,
): T | undefined => arr.find((item) => item.id === id);
export const _findItemWithId =
<T extends WithId>(id: string) =>
(arr: Array<T>): T | undefined =>
findItemWithId(arr, id);
/* --- Find Essential Item With Id -- */
export const findEssentialItemWithId = <T extends WithId>(
arr: Array<T>,
id: string,
): T | undefined => {
const item = findItemWithId(arr, id);
if (!item) {
throw new Error(`Could not find item with id ${id}`);
}
return item;
}
export const _findEssentialItemWithId =
<T extends WithId>(id: string) =>
(arr: Array<T>): T | undefined =>
findEssentialItemWithId(arr, id);
/* -- Sort items by reference array - */
export const sortItemsByReferenceArray = <T extends WithId>(
items: Array<T>,
referenceArray: Array<string>,
): Array<T> =>
[...items].sort(
(a, b) => referenceArray.indexOf(a.id) - referenceArray.indexOf(b.id),
);
export const _sortItemsByReferenceArray =
<T extends WithId>(referenceArray: Array<string>) =>
(items: Array<T>): Array<T> =>
sortItemsByReferenceArray(items, referenceArray);
import { atomWithHash } from "jotai-location";
import { SetStateAction, WritableAtom, useAtomValue, useSetAtom } from "jotai";
import { useCallback } from "react";
import { RESET } from "jotai/utils";
// If using a utility types library, it probably has it's own `Primitive` type.
type Primitive = string | number | boolean | null | undefined;
type DisappearingHashAtom<T> = WritableAtom<
T,
[SetStateAction<T> | typeof RESET],
void
> & {
paramName: string;
initialValue: T;
};
const usedKeys = new Set<string>();
/**
* An atom that's value is bound to a hash param and the url.
* If it's value ever returns to it's initial value, the hash param
* is removed from the url.
*/
export const disappearingHashAtom = <T extends Primitive>(
paramName: string,
initialValue: T,
): DisappearingHashAtom<T> => {
if (usedKeys.has(paramName)) {
throw new Error(
`There is already a disappearing hash atom using the parameter name "${paramName}"`,
);
}
usedKeys.add(paramName);
const atom = atomWithHash<T>(paramName, initialValue);
return Object.assign({}, atom, { initialValue, paramName });
};
export const uiIsOpenAtom = (key: string) =>
disappearingHashAtom<boolean>(key, false);
export const useSetDisappearingHashAtom = <T extends Primitive>(
theAtom: ReturnType<typeof disappearingHashAtom<T>>,
) => {
const baseSetValue = useSetAtom(theAtom);
const setValue = useCallback(
(newValue: T) => {
if (newValue === theAtom.initialValue) {
// RESET tells `jotai-location` to remove the hash param from the url
// entirely, and resets the atom's value to it's initial value.
baseSetValue(RESET);
} else {
baseSetValue(newValue);
}
},
[baseSetValue, theAtom],
);
return setValue;
};
export const useDisappearingHashAtom = <T extends Primitive>(
theAtom: ReturnType<typeof disappearingHashAtom<T>>,
) => {
const value = useAtomValue(theAtom);
const setValue = useSetDisappearingHashAtom(theAtom);
return [value, setValue] as const;
};
export const historyDisclosureAtom = (key: string) => {
const baseAtom = atomWithHash(key, false);
const prevValueAtom = atom(undefined as boolean | undefined);
const theAtom = atom(
(get) => get(baseAtom),
(get, set, newValue: boolean | typeof RESET) => {
const currentValue = get(baseAtom);
set(prevValueAtom, currentValue);
set(baseAtom, newValue);
},
);
return atom(
(get) => get(theAtom),
(get, set, newValue: boolean) => {
const prevValue = get(prevValueAtom);
if (!newValue && prevValue === false) {
window.history.back();
} else if (!newValue) {
set(theAtom, RESET);
} else {
set(theAtom, newValue);
}
},
);
};
export const useHistoryDisclosureAtom = (
theAtom: ReturnType<typeof historyDisclosureAtom>,
) => {
const [value, setValue] = useAtom(theAtom);
return [
value,
{ open: () => setValue(true), close: () => setValue(false) },
] as const;
};
import { useReducer } from "react";
// This import is only used for the optional react-hook
// integration (go to the bottom of the file to see it)
// `B` prefix denotes a `branded` type
type BNoPayload = {
JgJES6BF8uyaOwF1: "FY7eBhPYJlqOxuVp";
};
type BOptionalPayload = {
A7nWdXs0r5RLuHRf: "zPcrRNRIl4r5IHbA";
};
// Wrap your payload type in this if you want it to be optional
export type OptionalPayload<Payload> = Payload & BOptionalPayload;
type ExtractOptionalPayloadType<PossiblyBrandedPayload> =
PossiblyBrandedPayload extends OptionalPayload<infer UnbrandedPayload>
? UnbrandedPayload
: PossiblyBrandedPayload; // We know this isn't branded if this conditional is true
// If only type is provided, there will be no payload field.
// Wrap payload with `OptionalPayload` generic type to make
// it optional
export type Action<
Type extends string,
Payload = BNoPayload,
> = Payload extends BNoPayload
? {
type: Type;
}
: Payload extends BOptionalPayload
? {
type: Type;
payload?: ExtractOptionalPayloadType<Payload>;
}
: Required<{
type: Type;
payload: ExtractOptionalPayloadType<Payload>;
}>;
export type ReducerObject<State, StateAction extends Action<string, any>> = {
[TypeName in StateAction["type"]]: (
state: State,
action: Extract<StateAction, { type: TypeName }>,
) => State;
};
export const createReducer =
<State, StateAction extends Action<string, any>>(
reducerObject: ReducerObject<State, StateAction>,
) =>
<ActionType extends StateAction["type"]>(
state: State,
action: Extract<StateAction, { type: ActionType }>,
): State => {
const actionReducer = reducerObject[action.type];
return actionReducer(state, action);
};
// OPTIONAL: Custom version of React's `useReducer` that uses an
// object reducer
export const useStateReducer = <State, StateAction extends Action<any, any>>(
initialState: State,
reducerObject: ReducerObject<State, StateAction>,
) => useReducer(createReducer(reducerObject), initialState);
/**
* `PathOf` is a type that returns a string-type
* representing 'paths' to a records fields.
*/
/* eslint-disable @typescript-eslint/naming-convention */
type INTERNAL_ConditionalUnion<
Base,
Incoming,
Condition extends boolean
> = Condition extends true ? Base | Incoming : Base;
type INTERNAL_StringKeyOf<Obj> = keyof Obj & string;
type INTERNAL_DeepOmitOptional<Obj extends Record<any, unknown>> = {
[Key in keyof Obj as Obj[Key] extends Required<Obj>[Key]
? Key
: never]: Obj[Key] extends Record<any, unknown>
? INTERNAL_DeepOmitOptional<Obj[Key]>
: Obj[Key];
};
// We combine the previous path + the separator + the new key
// if the previous path is not an empty string. If the previous
// path is an empty string, just return the new key.
type INTERNAL_NextPath<
Previous extends string,
Separator extends string,
NewKey extends string
> = Previous extends '' ? NewKey : `${Previous}${Separator}${NewKey}`;
// The main functionality of `PathOf`, allows for certain
// aspects of the functionality to be customised via options
// passed as generics. For better DX, we will instead export
// different types that are just versions of this type with
// specific options applied. This approach also allows us to
// make sure the `Previous` key cannot be accessed as it is
// only intended to be provided internally during recursion
type INTERNAL_PathOf<
Obj extends Record<string, any>,
IncludeNonLeaves extends boolean,
Separator extends string = '.',
Previous extends string = ''
> = {
[Key in INTERNAL_StringKeyOf<Obj>]: Required<Obj>[Key] extends Record<
string,
any
>
? // $ If the value at `Key` is an object, we recurse down to the next level
INTERNAL_ConditionalUnion<
INTERNAL_PathOf<
Required<Obj>[Key],
IncludeNonLeaves,
Separator,
INTERNAL_NextPath<Previous, Separator, Key>
>,
// $ If we are allowing non-leaves, then the key can be either the current
// $ path, or the end result of the recursion. If we are only allowing leaves,
// $ then the path must be the end result of the recursion
INTERNAL_NextPath<Previous, Separator, Key>,
//? If this conditional is met, that means we are both returning the current
//? path as a valid option and continuing to recurse down to look for more
IncludeNonLeaves
>
: // $ If the value at `Key` is a primitive, we return the current path
INTERNAL_NextPath<Previous, Separator, Key>;
//? If this gets hit that means the recursion of the current branch is complete
//? And the current branch is returned as a valid path
}[INTERNAL_StringKeyOf<Obj>];
type PathOf<
Obj extends Record<string, any>,
Separator extends string = '.'
> = INTERNAL_PathOf<Obj, true, Separator>;
export type PathOfLeaf<
Obj extends Record<string, any>,
Separator extends string = '.'
> = INTERNAL_PathOf<Obj, false, Separator>;
export type NonOptionalPathOf<
Obj extends Record<string, any>,
Separator extends string = '.'
> = PathOf<INTERNAL_DeepOmitOptional<Obj>, Separator>;
export type NonOptionalPathOfLeaf<
Obj extends Record<string, any>,
Separator extends string = '.'
> = PathOfLeaf<INTERNAL_DeepOmitOptional<Obj>, Separator>;
export default PathOf;
/* #region Internal Utility Functions */
const generateArray = <Element>(
length: number,
generator: (i: number) => Element,
): Element[] => Array(length).map((_, index) => generator(index));
const randomInt = (min: number, max: number): number =>
Math.floor(Math.random() * (max - min + 1)) + min;
const capitalizeWord = (word: string): string => {
const [firstChar, ...otherChars] = word.split("");
const combined = [firstChar.toUpperCase(), ...otherChars].join("");
return combined;
};
/* #endregion */
const PLACEHOLDER_TEXT_WORDS = [
"lorem",
"ipsum",
"dolor",
"sit",
"amet",
"consectetur",
"adipiscing",
"elit",
"sed",
"do",
"eiusmod",
"tempor",
"incididunt",
"ut",
"labore",
"et",
"dolore",
"magna",
"aliqua",
"ut",
"enim",
"ad",
"minim",
"veniam",
"quis",
"nostrud",
"exercitation",
"ullamco",
"laboris",
"nisi",
"ut",
"aliquip",
"ex",
"ea",
"commodo",
"consequat",
"duis",
"aute",
"irure",
"dolor",
"in",
"reprehenderit",
"in",
"voluptate",
"velit",
"esse",
"cillum",
"dolore",
"eu",
"fugiat",
"nulla",
"pariatur",
"excepteur",
"sint",
"occaecat",
"cupidatat",
"non",
"proident",
"sunt",
"in",
"culpa",
"qui",
"officia",
"deserunt",
"mollit",
"anim",
"id",
"est",
"laborum",
];
export const generatePlaceholderWord = (): string => {
const index = randomInt(0, PLACEHOLDER_TEXT_WORDS.length - 1);
return PLACEHOLDER_TEXT_WORDS[index];
};
export const generatePlaceholderWords = (amount: number): string => {
const array = generateArray(amount, generatePlaceholderWord);
return array.join(" ");
};
export const generatePlaceholderSentence = (): string => {
const numberOfWords = randomInt(6, 9);
const firstWord = generatePlaceholderWord();
const tailWords = generatePlaceholderWords(numberOfWords - 1);
const words = `${capitalizeWord(firstWord)} ${tailWords}`;
return `${words}.`;
};
export const generatePlaceholderParagraph = (): string => {
const numberOfSentences = randomInt(4, 8);
const sentences = generateArray(
numberOfSentences,
generatePlaceholderSentence,
);
return sentences.join(" ");
};

Libs

This libs folder is for more complex gists that are effectively like mini libraries.

/**
* The idea behind a "storage atom" is to invert the
* way we use the JS storage APIs. Rather than using
* the base storage controller and accessing specific
* elements every time we use it, we instead create a
* new controller (the "atom") which then interfaces
* with the base API for us.
* */
const extractErrorMessage = (err: any): string => {
if (typeof err === "string" || typeof err === "number") {
return `${err}`;
}
const includedMessage = err.message;
if (includedMessage) {
if (
typeof includedMessage === "string" ||
typeof includedMessage === "number"
) {
return `${err}`;
}
return JSON.stringify(includedMessage);
}
return JSON.stringify(err);
};
export type StorageType = "local" | "session";
export const getControllerForStorageMode = (mode: StorageType) =>
mode === "local" ? localStorage : sessionStorage;
export type StorageAtom<Value> = {
set: (p: Value) => void;
get: () => Value;
remove: () => void;
reset: () => Value;
defaultValue: Value;
_meta: {
initializedAt: Date;
type: StorageType;
key: string;
};
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const stubFn = () => {};
const createDummyStorageAtom = <Value>(
type: StorageType,
key: string,
defaultValue: Value,
): StorageAtom<Value> => ({
remove: stubFn,
get: () => defaultValue,
reset: () => defaultValue,
set: stubFn,
defaultValue,
_meta: {
type,
initializedAt: new Date(),
key,
},
});
export type Stringifier<T> = (p: T) => string;
export type Parser<T> = (p: string) => T;
export type SerializationController<T> = {
stringify: Stringifier<T>;
parse: Parser<T>;
};
const getDefaultSerializationController = <
T,
>(): SerializationController<T> => ({
stringify: JSON.stringify,
parse: JSON.parse,
});
const createStorageAtom = <Value>(
type: StorageType,
key: string,
defaultValue: Value,
customSerializer: Partial<SerializationController<Value>> = {},
): StorageAtom<Value> => {
try {
const { stringify, parse } = {
...getDefaultSerializationController<Value>(),
...customSerializer,
};
const storageController = getControllerForStorageMode(type);
const get = (): Value => {
const rawValue = storageController.getItem(key);
if (!rawValue) {
return defaultValue;
}
try {
const parsed = parse(rawValue) as Value;
return parsed;
} catch (e) {
throw Error(
`An error occurred while trying to parse the value stored with key "${key}": ${extractErrorMessage(
e,
)}`,
);
}
};
const set = (newValue: Value) => {
const stringified = stringify(newValue);
storageController.setItem(key, stringified);
};
const remove = () => {
storageController.removeItem(key);
};
const reset = () => {
const stringified = stringify(defaultValue);
storageController.setItem(key, stringified);
return defaultValue;
};
return {
get,
set,
defaultValue,
remove,
reset,
_meta: {
initializedAt: new Date(),
key,
type,
},
};
} catch (e) {
return createDummyStorageAtom(type, key, defaultValue);
}
};
export default createStorageAtom;
// NOTE: For maximum functionality, replace usages of `JSON.parse`
// and `JSON.stringify` with the `superjson` package.
export type StorageController<Data, HasInitialValue extends boolean = false> = {
get: () => Data | (HasInitialValue extends true ? Data : undefined);
set: (data: Data) => Data;
} & (HasInitialValue extends true
? {
reset: () => Data;
}
: {
clear: () => undefined;
});
export type StorageControllerWithInitialValue<Data> = StorageController<
Data,
true
>;
export type StorageControllerWithoutInitialValue<Data> = StorageController<
Data,
false
>;
export function createStorageController<Data>( // No initial value provided
storageController: Storage,
key: string
): StorageControllerWithoutInitialValue<Data>;
export function createStorageController<Data>( // Initial value provided
storageController: Storage,
key: string,
initialData: Data
): StorageControllerWithInitialValue<Data>;
export function createStorageController<Data>( // Implementation
storageController: Storage,
key: string,
initialData?: Data
) {
const get = () => {
const data = storageController.getItem(key);
return JSON.parse(data ?? 'undefined');
};
const set = (data: Data) => {
storageController.setItem(key, JSON.stringify(data));
return get();
};
const clear = () => {
storageController.removeItem(key);
if (initialData)
throw new Error('Cannot clear storage controller with initial value');
return undefined;
};
const reset = () => {
if (!initialData)
throw new Error('Cannot reset storage controller without initial value');
return set(initialData);
};
if (initialData && get() === undefined) {
// Set with initial value if one exists and
// get() returns undefined
set(initialData);
}
return {
get,
set,
clear,
reset,
};
}
/**
* NOTE: If you need to use this to store complex structures (eg; Date,
* Map, Set, etc), you'll need to use an alternative to the vanilla
* `JSON.stringify/parse` methods. I recommend using `superjson`
*/
export type StorageUnit<T> = {
get: () => T;
set: (value: T) => void;
};
type StorageControllerType = "session" | "local";
type StorageController = Pick<
typeof localStorage & typeof sessionStorage,
"getItem" | "setItem"
>;
const getSessionController = <T>(
type: StorageControllerType,
defaultValue: T,
): StorageController => {
if (typeof window === "undefined")
// Use a mock storage controller if we're not in the browser
// (eg; SSR)
return {
getItem: () => JSON.stringify(defaultValue),
// eslint-disable-next-line @typescript-eslint/no-empty-function
setItem: () => {},
};
const storageController = type === "session" ? sessionStorage : localStorage;
return storageController;
};
export const createStorageUnit = <T>(
key: string,
defaultValue: T,
storageType: StorageControllerType,
): StorageUnit<T> => {
const storageController = getSessionController(storageType, defaultValue);
return {
get: () =>
JSON.parse(
storageController.getItem(key) ?? JSON.stringify(defaultValue),
) as T,
set: (value: T) => storageController.setItem(key, JSON.stringify(value)),
};
};
const theme = {
shadows: {
base: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)",
sm: "0 1px 2px 0 rgba(0, 0, 0, 0.05)",
md: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)",
lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)",
xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)",
twoXl: "0 25px 50px -12px rgba(0, 0, 0, 0.25)",
inner: "inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)",
none: "0 0 #0000;",
},
};
import { LiteralToPrimitive } from "type-fest";
import { z } from "zod";
type IsLiteral<PossiblyLiteral> =
LiteralToPrimitive<PossiblyLiteral> extends PossiblyLiteral ? false : true;
// Primitives do not extend descended literals, so if the primitive
// version of a type extends its "literal" version, then its "literal"
// version must have actually been primitive
type RawStringToZod<Str extends string> = IsLiteral<Str> extends true
? z.ZodLiteral<Str>
: z.ZodString;
type RawObjectToZod<Obj extends Record<string, unknown>> = z.ZodObject<{
[Key in keyof Obj]: RawToZod<Obj[Key]>;
}>;
/**
* Take a base typescript type and convert it to the corresponding
* zod schema type
* NOTE: This will not work for union types eg; `string | number`
*/
export type RawToZod<T> = z.ZodType<never> &
(T extends number
? z.ZodNumber
: T extends string
? RawStringToZod<T>
: T extends Record<string, unknown>
? RawObjectToZod<T>
: T extends (infer El)[]
? z.ZodArray<RawToZod<El>>
: T extends Date
? z.ZodDate
: T extends boolean
? z.ZodBoolean
: T extends undefined
? z.ZodUndefined
: T extends null
? z.ZodNull
: z.ZodTypeAny);
import { create } from "zustand";
import { combine } from "zustand/middleware";
const createState = <State extends Record<string, unknown>, Action>(
initialState: State,
reducer: (currentState: State, action: Action) => Partial<State>,
) =>
create(
combine(initialState, (set) => ({
dispatch: (action: Action) => set((state) => reducer(state, action)),
})),
);
export default createState;
import { StateCreator, create } from "zustand";
import { combine } from "zustand/middleware";
export class StateBuilder<State extends Record<string, unknown>> {
// eslint-disable-next-line no-useless-constructor, no-empty-function
constructor(private initialState: State) {}
actions<U extends Record<string, unknown>>(
actions: StateCreator<State, [], [], U>,
) {
return create(combine(this.initialState, actions));
}
}
/**
* Can adapt this to zustand's `slice` pattern by just not wrapping
* the `combine` call inside `create`.
*/
// # Example Usage
type CounterState = {
count: number;
};
const useCounterState = new StateBuilder<CounterState>({
count: 0,
}).actions((set) => ({
increment: () => set((state) => ({ count: state.count + 1 })),
setTo: (value: number) => set({ count: value }),
}));
const useA = () => {
const { increment, setTo } = useCounterState();
const count = useCounterState((state) => state.count);
};
import { D } from "@mobily/ts-belt";
import cuid from "cuid";
import produce, { Draft } from "immer";
import create from "zustand";
import { combine } from "zustand/middleware";
type RemoveFirstParam<
Fn extends (...args: [any, ...any[]]) => any
> = Fn extends (...args: [any, ...infer RemainingParams]) => infer ReturnType
? (...args: RemainingParams) => ReturnType
: never;
type StoreActionDefinition<State, ExtraParams extends any[]> = (
draftState: Draft<State>,
...extraParams: ExtraParams
) => void;
type CreatedStoreAction<
ActionDefinition extends StoreActionDefinition<any, any>
> = RemoveFirstParam<ActionDefinition>;
/**
* Provides a layer of abstraction for creating zustand stores
* and actions with high type safety and low boiler plate
*/
class StoreBuilder<
State extends Record<string, any>,
Actions extends Record<string, StoreActionDefinition<State, any[]>> = Record<
never,
never
>
> {
private initialState: State;
private actions: Actions;
/**
* Build the new store
*
* @param initialState the initial state of the store
* @param actions INTERNAL ONLY, DO NOT USE
* @param selectors INTERNAL ONLY, DO NOT USE
*/
constructor(
initialState: State,
actions: Actions = {} as any,
) {
// the "actions" and parameter should always default to {} when
// `new StoreBuilder` is called. It should only actually be provided
// inside the `addAction` method
this.initialState = initialState;
this.actions = actions;
}
public addAction<ActionName extends string, Params extends any[]>(
actionName: ActionName,
action: StoreActionDefinition<State, Params>
): StoreBuilder<
State,
Actions &
{
[key in ActionName]: StoreActionDefinition<State, Params>;
},
Selectors
> {
return new StoreBuilder(
this.initialState,
{
...this.actions,
[actionName]: action,
},
this.selectors
);
}
public getStore() {
return create(
combine(this.initialState, (set, get) => ({
...(D.map(
this.actions,
(actionFn) => (
...params: Parameters<RemoveFirstParam<typeof actionFn>>
) =>
set((currentState) => {
return produce(currentState, (draftState) =>
actionFn(draftState, ...params)
);
}) // We map over the actions in the store and convert them so that
// they read the state from the store rather than taking it as a parameter
) as {
[Key in keyof Actions]: CreatedStoreAction<Actions[Key]>;
}),
}))
);
}
}
// Source: https://github.com/pmndrs/zustand/discussions/2195
import { create, StateCreator, StoreMutatorIdentifier } from "zustand";
import { devtools } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
/**
* The convenience of this could be futher enhanced by using a class
* and implementing something similar to redux-toolkits chaining reducer
* syntax.
*
* https://redux-toolkit.js.org/api/createreducer#usage-with-the-builder-callback-notation
*/
export function createOurStore<T extends object, U extends object>(
name: string,
state: T,
actions: StateCreator<
T,
[["zustand/devtools", never], ["zustand/immer", never]],
[["zustand/immer", never], ["zustand/devtools", never]],
U
>,
) {
return create<T & U>()(
devtools(
immer((...a) => Object.assign({}, state, (actions as any)(...a))),
{ name },
),
);
}
// # Example
const useOurStore = createOurStore("foo", { name: "" }, (set) => ({
setName: (name: string) =>
set((state) => {
state.name = name;
}),
}));
import { isEqual } from 'lodash'; // NOTE: Replace with your own isEqual function if not using lodash
type AnyAsyncFunction = (...params: any[]) => Promise<any>;
type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
type MemoCacheEntry<Fn extends AnyAsyncFunction> = {
params: Parameters<Fn>;
result: Awaited<ReturnType<Fn>>;
};
export const memoAsync = <Fn extends AnyAsyncFunction>(
fetcher: Fn,
compareParams: (
prevParams: Parameters<Fn>,
nextParams: Parameters<Fn>
) => boolean = isEqual
): Fn => {
const cache: MemoCacheEntry<Fn>[] = [];
const finalFunction = async (...params: Parameters<Fn>) => {
const cacheEntry = cache.find((entry) =>
compareParams(entry.params, params)
);
if (cacheEntry) {
return cacheEntry.result as ReturnType<Fn>;
}
const result = await fetcher(...params);
cache.push({ params, result });
return result;
};
return finalFunction as any;
};

Helpful Regex

All the text within string values in a JSON file, will not match the keys of the JSON:

/:\s*"(.*?)"/g;

If you want to delete all the text within the quotes while still retaining the quotes themselves so we still have valid strings, you can replace all instances of that pattern with this: : ""

// Take an object and convert it to a string that can
// be used as URL parameters
const objToUrlParams = <
Params extends Record<string, number | string> = Record<
string,
number | string
>,
>(
obj: Params,
) =>
Object.entries(obj)
.map(([key, value]) => `${key}=${value}`)
.join("&");
export default objToUrlParams;
export function pipe<A, B>(a: A, ab: (a: A) => B): B;
export function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C;
export function pipe<A, B, C, D>(
a: A,
ab: (a: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
): D;
export function pipe<A, B, C, D, E>(
a: A,
ab: (a: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
): E;
export function pipe<A, B, C, D, E, F>(
a: A,
ab: (a: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
): F;
export function pipe<A, B, C, D, E, F, G>(
a: A,
ab: (a: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G,
): G;
export function pipe<A, B, C, D, E, F, G, H>(
a: A,
ab: (a: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G,
gh: (g: G) => H,
): H;
export function pipe<A, B, C, D, E, F, G, H, I>(
a: A,
ab: (a: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G,
gh: (g: G) => H,
hi: (h: H) => I,
): I;
export function pipe<A, B, C, D, E, F, G, H, I, J>(
a: A,
ab: (a: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G,
gh: (g: G) => H,
hi: (h: H) => I,
ij: (i: I) => J,
): J;
export function pipe<A, B, C, D, E, F, G, H, I, J, K>(
a: A,
ab: (a: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G,
gh: (g: G) => H,
hi: (h: H) => I,
ij: (i: I) => J,
jk: (j: J) => K,
): K;
export function pipe<A, B, C, D, E, F, G, H, I, J, K, L>(
a: A,
ab: (a: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G,
gh: (g: G) => H,
hi: (h: H) => I,
ij: (i: I) => J,
jk: (j: J) => K,
kl: (k: K) => L,
): L;
export function pipe<A, B, C, D, E, F, G, H, I, J, K, L, M>(
a: A,
ab: (a: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G,
gh: (g: G) => H,
hi: (h: H) => I,
ij: (i: I) => J,
jk: (j: J) => K,
kl: (k: K) => L,
lm: (l: L) => M,
): M;
// If you need to support more params you can just keep adding
// overloads here. The actual implementation is not affected
// by the number of params so you don't need to update it.
export function pipe(a: unknown, ...fns: ((a: unknown) => unknown)[]): unknown {
return fns.reduce((acc, fn) => fn(acc), a);
}
type PrioritisedState<T> = {
value: T;
isValid: boolean;
};
const stateIsValid = (state: PrioritisedState<unknown>) => state.isValid;
// If you have multiple different distinct UI states
// that may have overlap in their conditions and which
// you only ever want to show one of, this function
// allows you to determine which state to show.
const prioritisedStates = <T>(states: PrioritisedState<T>[], fallback: T) => {
const firstValid = states.find(stateIsValid);
return firstValid?.value ?? fallback;
};
export default prioritisedStates;
// example usage
type DummyImageData = {
src?: string;
directURL?: string;
};
const FALLBACK_IMAGE_SRC = 'https://example.com/fallback.jpg';
const getImageFileSrc = (imageData: DummyImageData): string =>
prioritisedStates<string>(
[
{
value: String(imageData.src),
isValid: !!imageData.src,
},
{
value: String(imageData.directURL),
isValid:
!!imageData.directURL &&
!imageData.directURL?.includes('localhost') &&
!imageData.directURL?.startsWith('blob:'),
},
],
FALLBACK_IMAGE_SRC
);
const NO_PAYLOAD = Symbol("NO_PAYLOAD");
type NoPayload = typeof NO_PAYLOAD;
type EmptyObject = Record<string, never>;
export type Action<Type extends string, Payload = NoPayload> = {
type: Type;
} & (Payload extends NoPayload ? EmptyObject : { payload: Payload });
type ExtractActionWithPayload<A extends Action<string, any>> = Extract<
A,
{
payload: unknown;
}
>;
type ExtractActionWithoutPayload<A extends Action<string, any>> = Exclude<
A,
ExtractActionWithPayload<A>
>;
type FilterActionByType<
A extends Action<string, any>,
T extends A["type"],
> = Extract<A, { type: T }>;
export const actionFactoryBuilder = <
AcceptedAction extends Action<string, any>,
>() => {
function composeAction<
SpecificActionType extends ExtractActionWithoutPayload<AcceptedAction>["type"],
>(
type: SpecificActionType,
): FilterActionByType<AcceptedAction, SpecificActionType>;
function composeAction<
SpecificActionType extends ExtractActionWithPayload<AcceptedAction>["type"],
>(
type: SpecificActionType,
payload: FilterActionByType<AcceptedAction, SpecificActionType>["payload"],
): FilterActionByType<AcceptedAction, SpecificActionType>;
function composeAction(type: string, payload?: unknown) {
return {
type,
...(payload === undefined ? {} : { payload }),
} as any;
}
return composeAction;
};
// Examples...
type CounterAction =
| Action<"INCREMENT">
| Action<"DECREMENT">
| Action<"SET", number>;
const counterAction = actionFactoryBuilder<CounterAction>();
counterAction("INCREMENT");
counterAction("SET", 5);
const removeFromString = (src: string, target: string) =>
src.replace(new RegExp(target, 'g'), "");
export default removeFromString;
/**
* A 'reverse debounce' is like a standard debounce,
* except it fires the callback immediately, and then
* blocks any further calls for a specified amount
* of time.
*
* The use case for this is when you want to fire a
* callback immediately for the best user experience
* while also preventing any duplicate calls.
*
* Can optionally pass a third parameter to set a
* maximum possibly delay between function calls.
*/
const reverseDebounce = <Params extends any[]>(
fn: (...p: Params) => void,
wait: number,
maxPossibleDelay: number = Number.MAX_SAFE_INTEGER
) => {
let lastRunAt = Date.now();
let blockedUntil = Date.now() - 1;
return (...params: Params): void => {
const timeSinceLastExecution = Date.now() - lastRunAt;
const maxDelayHasBeenViolated = timeSinceLastExecution >= maxPossibleDelay;
const blockingWindowHasPassed = Date.now() >= blockedUntil;
const canRun = blockingWindowHasPassed || maxDelayHasBeenViolated;
if (canRun) {
fn(...params);
lastRunAt = Date.now();
}
blockedUntil = Date.now() + wait;
};
};
export default reverseDebounce;
const roundNumber = (number: number, decimalPoints: number) => {
const factor = Math.max(10 ** decimalPoints, 1);
return Math.round(number * factor) / factor;
};
export default roundNumber;
const roundToInterval = (value: number, interval: number): number =>
Math.round(value / interval) * interval;
export default roundToInterval;
const insertAt = (str: string, index: number, value: string) =>
str.substr(0, index) + value + str.substr(index);
const getDigitsRightOfDecimal = (num: number) => {
const stringNum = String(num);
const decimalPosition = stringNum.indexOf(".");
return decimalPosition === -1 ? 0 : stringNum.length - decimalPosition - 1;
};
const multiplyByPowerOf10 = (num: number, power: number): number => {
if (power === 0) return num;
const stringified = String(num);
const originalDecimalPosition =
stringified.length - 1 - getDigitsRightOfDecimal(num);
const noDecimalString = String(num).replace(".", "");
const newDecimalPosition = originalDecimalPosition + power;
const newString = noDecimalString.padEnd(newDecimalPosition, "0");
return Number(newString);
};
const divideByPowerOf10 = (num: number, power: number): number => {
if (power === 0) return num;
const stringified = String(num);
const originalDecimalPosition =
stringified.length - 1 - getDigitsRightOfDecimal(num);
const noDecimalString = String(num).replace(".", "");
const newDecimalPosition = originalDecimalPosition - power + 1;
const newString = insertAt(noDecimalString, newDecimalPosition, ".");
return Number(newString);
};
/**
* Perform any math operation on two numbers, without losing precision.
* Will likely fail if dealing with numbers with more than 15 digits.
*/
export const safeMath = (
a: number,
b: number,
operation: (numA: number, numB: number) => number,
): number => {
const decimalsToRightInA = getDigitsRightOfDecimal(a);
const decimalsToRightInB = getDigitsRightOfDecimal(b);
const mustMultiplyByPower = Math.max(decimalsToRightInA, decimalsToRightInB);
const intA = multiplyByPowerOf10(a, mustMultiplyByPower);
const intB = multiplyByPowerOf10(b, mustMultiplyByPower);
const result = operation(intA, intB);
return divideByPowerOf10(result, mustMultiplyByPower);
};
const sleep = (time: number) => {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < time);
};
export default sleep;
export type StrictAny =
| number
| string
| Date
| boolean
| null
| undefined
| object
| bigint
| Map<StrictAny, StrictAny>
| Set<StrictAny>
| symbol
| StrictAny[]
| {
[key: string]: StrictAny;
};
const toPromise = <T>(value: T): Promise<T> => new Promise((resolve) => resolve(value));
export default toPromise;

TSConfig Setup

Paths

The paths compiler option in tsconfig.json allows you to setup import aliases to organise your imports.

{
	"compilerOptions": {
		"baseUrl": ".",
		"paths": {
			"$/*": ["./src/*"],
			"$*": ["./src/features/*", "./src/lib/*"]
		}
	}
}

This setup defines the following import aliases:

Path Import Alias
src/*folder-name* $/*folder-name*
src/features/*feature-name* $*feature-name*
src/features/*feature-name*/*folder-name* $*feature-name*/*folder-name*
src/lib/*lib-name* $*lib-name*

Usage of Paths (IMPORTANT)

Depending on what build tools your project is using, you may run into issues with the way it handles your custom paths. This can lead to issues where files are compiled in incorrect order, which causes imports from other files to be imported as undefined. To avoid this, your import statements should follow these rules:

  • All imports of resources not in a feature folder should be imported using their folder's index file using the $/folderName syntax. Eg; an import of src/utils/generateId.ts should look like this:
import { generateId } from "$/utils";
  • Importing resources from a feature folder into a file that is either not in a feature folder, or in a different feature folder should be imported from that feature folder's index. Eg; importing src/features/authentication/utils/signIn.ts into src/components/SignInForm.tsx would look like this:
// src/components/SignInForm.tsx
import { signIn } from "$authentication";
  • Importing a file from a feature folder into another file in the same feature folder should use the path structure of $feature/subFolder. Eg; importing src/features/calendar/utils/getEvents.ts into the file src/features/calendar/components/Calendar.tsx would ook like this:
// src/features/calendar/components/Calendar.tsx
import { getEvents } from "$calendar/utils";

Compatibility

Just about every tool that interacts in some way with your code will require additional setup to work correctly with your tsconfig path setups. Tools that will likely require extra setup include:

  • Build tools (webpack, vite, etc.)
  • Test runners (jest) *testing packages that do not need to actually interpret your code such as cypress and playwright may not need additional setup)
  • Linters that analyze imports (eslint-plugin-imports)

The setup required for each of these can be found relatively easily. Setup may require either manually defining import aliases that match your tsconfig, or using some kind of tool that integrates your tsconfig into the tool.

Extra Setup for Node

To get paths to work with ts-node you must also do the following steps:

Run this:

yarn add --dev tsconfig-paths

Then add this to your tsconfig:

{
	"ts-node": {
		"require": ["tsconfig-paths/register"]
	}
}

Enforce With Eslint

If you want to enforce the shortest possible paths are used when importing from 'lib' or 'features' you can add this to your eslint config:

{
	rules: {
		'no-restricted-imports': [
      'error',
      {
        patterns: [
          {
            group: ['$/features/**', '$/lib/**'],
            message: 'Use short-form path instead ("$lib-name" or "$feature-name")',
          },
        ],
      },
    ],
	}
}

Running Scripts

Install

First of all install ts-node as a dev dependency (if you don't have/need it as a regular dependency)

yarn add --dev ts-node

Config

In your tsconfig.json, do 2 things:

  • Make sure the includes option includes the folder that contains your script files.
  • Add the following to the bottom of the file:
{
	"ts-node": {
		"compilerOptions": {
			"module": "CommonJS"
		}
	}
}

Executing Scripts

Now you can write scripts in typescript and run them using ts-node

yarn ts-node scripts/yourScripts.ts
export type PickOptionalFields<T> = {
[K in keyof T]-?: undefined extends T[K] ? K : never;
}[keyof T];
export type OmitOptionalFields<T> = Pick<
T,
Exclude<keyof T, PickOptionalFields<T>>
>;
/**
* Returns `never` if any of the fields in `T` are optional.
*/
type NoOptionalFieldsAllowed<T> = Exclude<
keyof T,
keyof OmitOptionalFields<T>
> extends never
? T
: never;
import { A } from "@mobily/ts-belt";
type UpsertUpdateResolver<T> = (base: T, update: T) => T;
/**
* "Upsert" an element into an array, overwriting an existing element if it
* exists, or adding it to the end of the array if it does not.
* @param arr The array to upsert into
* @param element The element to upsert
* @param equalityFn A function that determines whether or not two elements are
* @param [updateResolver] A function is used to determine how to apply an
* update if the element already exists in the array. Receives the existing
* element as it's first param and the "update" element (the second param of
* this entire upsert function) as it's second param, and must return a new
* element to replace the existing element with. If not provided, the "update"
* element will simply replace the existing element.
*/
function upsert<T>(
arr: T[],
element: T,
equalityFn: (a: T, b: T) => boolean,
): T[];
function upsert<T>(
arr: T[],
element: T,
equalityFn: (a: T, b: T) => boolean,
updateResolver: UpsertUpdateResolver<T>,
): T[];
function upsert<T>(
element: T,
equalityFn: (a: T, b: T) => boolean,
): (arr: T[]) => T[];
function upsert<T>(
element: T,
equalityFn: (a: T, b: T) => boolean,
updateResolver: UpsertUpdateResolver<T>,
): (arr: T[]) => T[];
function upsert<T>(
...args:
| [T[], T, (a: T, b: T) => boolean]
| [T[], T, (a: T, b: T) => boolean, UpsertUpdateResolver<T>]
| [T, (a: T, b: T) => boolean]
| [T, (a: T, b: T) => boolean, UpsertUpdateResolver<T>]
) {
if (Array.isArray(args[0])) {
const [arr, element, equalityFn, resolver = (_, update) => update] =
args as [
T[],
T,
(a: T, b: T) => boolean,
UpsertUpdateResolver<T> | undefined,
];
const index = arr.findIndex((item) => equalityFn(item, element));
const elementIsNotAlreadyInArray = index === -1;
if (elementIsNotAlreadyInArray) {
return [...arr, element];
}
return A.updateAt(arr, index, (base) => resolver(base, element));
}
const [element, equalityFn, resolver] = args as [
T,
(a: T, b: T) => boolean,
UpsertUpdateResolver<T> | undefined,
];
return (arr: T[]) =>
resolver
? upsert(arr, element, equalityFn, resolver)
: upsert(arr, element, equalityFn);
}
export default upsert;
import { useCallback, useMemo, useState } from "react";
import { DeepMutable, produce } from "@/utils/produce";
export const usePendingState = <State, Action>(
baseState: State,
reducer: (state: State, action: Action) => State,
) => {
const [pendingActions, setPendingActions] = useState<Action[]>([]);
const dispatch = useCallback(
(newAction: Action) => {
setPendingActions(
produce((actions) => {
actions.push(newAction as any);
}),
);
},
[setPendingActions],
);
const clearPendingActions = useCallback(() => {
setPendingActions([]);
}, [setPendingActions]);
const pendingState = useMemo(
() => pendingActions.reduce(reducer, baseState),
[pendingActions, baseState, reducer],
);
const popLastAction = useCallback(() => {
setPendingActions(
produce((actions) => {
actions.pop();
}),
);
}, [setPendingActions]);
};
const UNDO = Symbol("UNDO");
const REDO = Symbol("REDO");
type Undo = typeof UNDO;
type Redo = typeof REDO;
type SpecialHistoryAction = Undo | Redo;
type StateHistoryTree<State> = {
current: State;
undoStack: State[];
redoStack: State[];
};
export const useReducerWithHistory = <State, Action>(
baseState: State,
reducer: (state: State, action: Action) => State,
) => {
const [stateTree, setStateTree] = useState<StateHistoryTree<State>>({
current: baseState,
undoStack: [],
redoStack: [],
});
const treeReducer = useCallback(
(
currentStateTree: StateHistoryTree<State>,
action: Action | SpecialHistoryAction,
): StateHistoryTree<State> =>
produce(currentStateTree, (draftState) => {
if (action === UNDO) {
const previousState = draftState.undoStack.pop();
if (previousState) {
draftState.redoStack.push(draftState.current);
draftState.current = previousState;
}
return;
}
if (action === REDO) {
const nextState = draftState.redoStack.pop();
if (nextState) {
draftState.undoStack.push(draftState.current);
draftState.current = nextState;
}
return;
}
draftState.undoStack.push(draftState.current);
draftState.current = reducer(
draftState.current as State,
action,
) as DeepMutable<State>;
draftState.redoStack = [];
}),
[reducer],
);
const dispatch = useCallback(
(action: Action | SpecialHistoryAction) => {
const newStateTree = treeReducer(stateTree, action);
setStateTree(newStateTree);
},
[treeReducer, stateTree],
);
return [stateTree.current, dispatch] as const;
};
/**
* NOTE: Types added to this Gist should avoid referencing each other
* where ever possible. This is to make it so types from this Gist can
* be easily copied individually into other projects without having to
* copy the entire Gist.
*/
// ## LOW-LEVEL VALUE TYPES
/* These are static types that take no
* no arguments and represent
* primitives */
export type PrimitiveValue = string | number | boolean | null;
export type PrintableValue = PrimitiveValue | undefined;
/**
* When using `keyof` on union types that do not share any
* fields, `keyof` returns `never`. This type is a workaround
* for that behaviour, and will return the union of all the
* keys of the passed type arguments.
*/
export type AnyKeyOfUnion<T> = T extends T ? keyof T : never;
// All the possible values that can be returned
// by the `typeof` keyword
export type TypeofReturn =
| "string"
| "number"
| "bigint"
| "boolean"
| "symbol"
| "undefined"
| "object"
| "function";
export type Falsy = false | 0 | "" | 0n | null | undefined;
// `NaN` is also falsy, however typescript has
// special behaviour for `NaN` that does not
// allow it to be references as a type literal
// ## OBJECT TYPES
export type AnyObject = Record<string, unknown> | unknown[];
export type EmptyObject = Record<never, never> | [];
// ## VALUE COMPOSITION TYPES
/* These types take type arguments and use
them to compose more complex types */
export type Dictionary<T> = Record<string, T>;
export type NonEmptyArray<T> = [T, ...T[]];
/* This is not actually particularly
useful. The idea behind this was to
use it as the type of a function's
parameter, but in an actual codebase,
arrays that we know aren't empty
will probably still be typed with the
standard `[]` typing, so passing it to
a function that expects `NonEmptyArray`
will trigger a typescript warning */
/**
* Widens typings for literals and tuples to their
* primitive types
*/
type Widen<T> = T extends string
? string
: T extends number
? number
: T extends boolean
? boolean
: T extends any[]
? Widen<T[number]>[]
: T;
// A value stored in a passed array or object
// type
export type ValueFrom<T> = T extends unknown[] | readonly unknown[]
? T[number]
: T[keyof T];
export type ObjectKeyValuePairs<Dict extends Record<string, unknown>> =
ValueFrom<{
[Key in keyof Dict]: [Key, Dict[Key]];
// This does look a bit weird, but doing it this way means that
// typescript can infer the type of the value item from the key
// Eg; if an object has a `number` at the `"value"` key, then
// if you do `pair[0] === "key"` typescript will then infer that
// `pair[1]` is a `number`
}>;
export type DeepPartial<TheObject extends Record<string, unknown>> = {
[Key in keyof TheObject]?: TheObject[Key] extends Record<string, unknown>
? DeepPartial<TheObject[Key]>
: TheObject[Key];
};
// Alternate version of `Extract` that enforces overlap between
// the type arguments
export type StrictExtract<Base, Sub extends Base> = Extract<Base, Sub>;
// Version of `Omit` where the keys being omitted
// must actually be keys of the base type. This
// should be used instead of `Omit` unless you are
// dealing with dynamic types and can't be sure
// that that rule will be followed.
export type StrictOmit<BaseType, ToOmit extends keyof BaseType> = Omit<
BaseType,
ToOmit
>;
// Take an object type and make specific fields
// non-optional
export type RequireSpecificKeys<
TheObject extends Record<string, unknown>,
Keys extends keyof TheObject,
> = TheObject & Required<Pick<TheObject, Keys>>;
// Take an object type and make specific fields
// optional
export type PartialSpecificKeys<
TheObject extends Record<string, unknown>,
Keys extends keyof TheObject,
> = Omit<TheObject, Keys> & Partial<Pick<TheObject, Keys>>;
// Removes the `readonly` property from a type
export type Mutable<Obj extends AnyObject> = {
-readonly [P in keyof Obj]: Obj[P];
};
export type ShallowMerge<
Source extends Record<string, any>,
Update extends Record<string, any>,
> = Omit<Source, keyof Update> & Update;
// Replace the type of a specific field(s)
// in an object type
export type ReplaceFieldType<
Dict extends Record<string, any>,
Key extends keyof Dict,
Replacement,
> = Omit<Dict, Key> & Record<Key, Replacement>;
// Get keys of the base type that are of a specific type
export type ExtractKeyOf<BaseType, KeyExtraction> = Extract<
keyof BaseType,
KeyExtraction
>;
export type StringKeyOf<BaseType> = ExtractKeyOf<BaseType, string>;
export type ExcludeKeyOf<BaseType, KeyExclusion> = Exclude<
keyof BaseType,
KeyExclusion
>;
export type PrefixKeys<
Dict extends Record<string, unknown>,
Prefix extends string,
> = {
[Key in StringKeyOf<Dict> as `${Prefix}${Key}`]: Dict[Key];
};
export type ConditionalUnion<
Base,
Incoming,
Condition extends boolean,
> = Condition extends true ? Base | Incoming : Base;
// Invert a boolean type
export type Not<Bool extends boolean> = Bool extends true ? false : true;
export type DeepOmitOptional<Obj extends Record<any, unknown>> = {
[Key in keyof Obj as Obj[Key] extends Required<Obj>[Key]
? Key
: never]: Obj[Key] extends Record<any, unknown>
? DeepOmitOptional<Obj[Key]>
: Obj[Key];
};
/**
* Pick some keys from an object type and make those
* fields required, except for some exceptions which
* will simply be picked without being marked as
* required
*/
export type PickAndMakeRequiredWithExceptions<
Base,
KeysToPick extends keyof Base,
KeysToNotMakeRequired extends KeysToPick,
> = Pick<Base, KeysToNotMakeRequired> &
Required<Omit<Pick<Base, KeysToPick>, KeysToNotMakeRequired>>;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export type KeyOfUnion<T> = T extends any ? keyof T : never;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
export type GetFieldValueFromUnion<T, K extends keyof T> = T extends any
? K extends keyof T
? T[K]
: never
: never;
/**
* Performs a 1-layer deep object merge where the types are listed
* as unions rather than being passed as generic type arguments.
* Useful for merging a dynamic number of object types.
*/
export type MergeUnion<T> = {
[K in KeyOfUnion<T>]: GetFieldValueFromUnion<T, K>;
};
/**
* An object that must have at least one field from the
* passed object type. Can have multiple fields.
*
* USE CASE: You have a component that has some dynamic
* sizing logic, but requires that either width or height
* be specified, but also allows for both to be specified.
* `AtLeastOneField<{ width: number, height: number }>`
*/
export type AtLeastOneField<Obj extends Record<string, unknown>> = ValueFrom<{
[K in keyof Obj]: Pick<Obj, K> & Partial<Omit<Obj, K>>;
}>;
/**
* Must match an object type exactly, or be an empty object.
*
* USE CASE: You may have an instance where an API has some
* optional params, but some are grouped, so if one param is
* provided then others must be as well. Eg; in the BTC bridge
* API, if you make use of the optional `parentEnity` param,
* then you must must also provide the `peid` param, but you
* can also provide neither, so you could use this type to
* make sure that if either `parentEntity` or `peid` is
* provided, then both must be provided.
*/
export type AllOrNone<T> = T | { [K in keyof T]?: never };
// ## FUNCTION TYPES
// Can be either a static value, or a getter
// function for deriving a value
export type GetterOrStatic<ReturnType, Params extends any[] = []> =
| ((...params: Params) => ReturnType)
| ReturnType;
export type Comparator<T> = (a: T, b: T) => boolean;
export type SortComparator<T> = (a: T, b: T) => number;
// ## MISC TYPES
// When you want to specify auto-complete suggestions in
// the editor while also allowing values outside of the
// suggestions to be provided.
// NOTE: Trust me, the use of `Omit` is not a mistake, for
// whatever reasons you have to use it instead of `Exclude`
// for this to work with strings. Not sure about other types
// as I have only tested with strings.
type WithLooseAutocomplete<AcceptedType, Suggestions extends AcceptedType> =
| Suggestions
| Exclude<Omit<AcceptedType, Suggestions & keyof AcceptedType>, Suggestions>;
type ValidJSON =
| string
| number
| boolean
| null
| ValidJSON[]
| { [key: string]: ValidJSON };
// If you are using the `superjson` package...
type ValidSuperJson =
| string
| number
| boolean
| undefined
| null
| bigint
| Date
| RegExp
| Error
| URL
| ValidSuperJson[]
| { [key: string]: ValidSuperJson }
| Map<string, ValidSuperJson>
| Set<ValidSuperJson>;
/**
* Use a map to essentially create a switch statement
* specifically for returning values based on the
* input's value.
*/
const valueSwitch = <Predicate, Return>(
value: Predicate,
cases: [Predicate, Return][]
): Return | undefined => new Map(cases).get(value);
/**
* Value switch that throws an error if the value
* is not found in the map.
*/
export const unsafeValueSwitch = <Predicate, Return>(
value: Predicate,
cases: [Predicate, Return][],
): Return => {
const foundValue = valueSwitch(value, cases);
if (!foundValue) throw Error(`Value '${value}' not found in cases `);
return foundValue;
};
export default valueSwitch;
export type ValueWithSimpleStub = number | string | any[];
export const withStringFallback = (value: string | undefined) => value ?? '';
export const withNumberFallback = (value: number | undefined) => value ?? 0;
export const withArrayFallback = <T>(value: T[] | undefined) =>
value ?? ([] as T[]);
// eslint-disable-next-line max-classes-per-file
import { z } from 'zod';
export type ZodSchemaMigration<
FromSchema extends z.ZodTypeAny,
ToSchema extends z.ZodTypeAny
> = {
to: ToSchema;
from: FromSchema;
up: (previousVersionData: z.infer<FromSchema>) => z.infer<ToSchema>;
down: (nextVersionData: z.infer<ToSchema>) => z.infer<FromSchema>;
};
const composeMigration = <
FromSchema extends z.ZodTypeAny,
ToSchema extends z.ZodTypeAny
>(
migration: ZodSchemaMigration<ToSchema, FromSchema>
) => migration;
const v1StateSchema = z.object({
person: z.object({
name: z.string(),
pet: z.object({
name: z.string(),
}),
}),
});
const v2StateSchema = z.object({
person: z.object({
name: z.string(),
}),
pet: z.object({
name: z.string(),
}),
});
export class MigrationController<FinalSchema extends z.ZodTypeAny> {
private legacyMigrations: ZodSchemaMigration<any, any>[] = [];
private finalMigration: ZodSchemaMigration<any, FinalSchema>;
// This constructor is only used internally
constructor(schema: FinalSchema);
constructor(finalMigration: ZodSchemaMigration<any, FinalSchema>);
constructor(
...params:
| [FinalSchema]
| [ZodSchemaMigration<any, FinalSchema>, ZodSchemaMigration<any, any>[]]
) {
if (params.length === 1) {
const [schema] = params;
this.finalMigration = {
from: z.any,
to: schema,
up: (data) => data,
down: (data) => data,
};
} else {
const [finalMigration, previousMigrations] = params;
this.finalMigration = finalMigration;
this.legacyMigrations = previousMigrations;
}
}
public addMigration<NewFinalSchema extends z.ZodTypeAny>(
migration: Omit<ZodSchemaMigration<FinalSchema, NewFinalSchema>, 'from'>
) {
const newMigration: ZodSchemaMigration<FinalSchema, NewFinalSchema> = {
from: this.finalMigration.to,
...migration,
};
return new MigrationController<NewFinalSchema>(
newMigration,
this.legacyMigrations.concat(this.finalMigration)
);
}
public parse(data: any): z.infer<FinalSchema> {
const readyForFinalMigration = this.legacyMigrations.reduce(
(result, migration) => {
try {
const fromData = migration.from.parse(result);
return migration.up(fromData);
} catch (e) {
return result;
}
},
data
);
return this.finalMigration.to.parse(readyForFinalMigration);
}
}
const a = new MigrationController(v1);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment