Skip to content

Instantly share code, notes, and snippets.

@tlux
Last active October 13, 2023 11:47
Show Gist options
  • Save tlux/043ab5cc8ad86a29dfcce5dce11ba6cb to your computer and use it in GitHub Desktop.
Save tlux/043ab5cc8ad86a29dfcce5dce11ba6cb to your computer and use it in GitHub Desktop.
Some Typescript utility functions for the real world
/**
* Creates a deep clone of an object.
*
* Internally uses `structuredClone` when available and
* `JSON.parse(JSON.stringify(obj))` as fallback.
*
* @param obj - The object to be cloned.
* @return The deep clone of the object.
*/
export default function cloneDeep<T>(value: T): T {
if (typeof structuredClone === "undefined") {
return JSON.parse(JSON.stringify(value));
}
return structuredClone(value);
}
/**
* Filters out any undefined or null values from the given list.
* Returns a new array.
*
* @param list - The list to be compacted.
* @return The compacted list.
*/
export default function compact<T>(list: T[]) {
return list.filter((item) => item != null) as NonNullable<T>[];
}
/**
* A function that constrains the values object to have only the keys that are
* present in the defaults object. It returns a new object that contains only
* the keys from values that are also present in defaults, with their
* corresponding values.
*
* @param defaults - An object representing the default values.
* @param values - An object representing the values to be combined with the
* default values.
* @return An object that contains the combined properties of `defaults` and
* `values`.
*/
export default function constrain<T extends Record<string, unknown>>(
defaults: T,
values: Partial<T>,
): T {
return Object.keys(defaults).reduce((acc, key: keyof T) => {
const value = values[key];
acc[key] = typeof value !== 'undefined' ? value : defaults[key];
return acc;
}, {} as T);
}
type Predicate<T> = (value: T, index: number, array: T[]) => boolean;
/**
* Finds the indices of elements in an array that satisfy a given condition.
*
* @param array The array to search through.
* @param predicate The condition that an element must satisfy.
* @return An array containing the indices of elements that satisfy the condition.
*/
export default function findIndices<T>(array: T[], predicate: Predicate<T>) {
const indices: number[] = [];
array.forEach((value, index) => {
if (!predicate(array[i], i, array)) return;
indices.push(i);
});
return indices;
}
/**
* Checks if the given object has a specific property.
*
* @param obj - The object to check.
* @param key - The property key to check for.
* @return Returns true if the object has the property, false otherwise.
*/
export default function has<
T extends Record<keyof any, unknown>,
K extends keyof T,
>(obj: T, key: K): obj is T & { [P in K]-?: T[P] } {
return key in obj;
}
/**
* Checks if a value is a boolean.
*
* @param value - The value to check.
* @return Returns true if the value is a boolean, otherwise returns false.
*/
export default function isBoolean(value: unknown): value is boolean {
return typeof value === 'boolean';
}
/**
* Checks if a given value is an object with string keys and any values.
*
* @param value - The value to check.
* @return Returns true if the value is an object with string keys and any
* values, otherwise false.
*/
export default function isFieldValues(
value: unknown,
): value is Record<string, any> {
return isRecord(value) && Object.getOwnPropertySymbols(value).length === 0;
}
/**
* Checks if a value is a float.
*
* @param value - The value to check.
* @return Returns true if the value is a float, otherwise returns false.
*/
export default function isFloat(value: unknown): value is number {
return typeof value === 'number' && !Number.isInteger(value);
}
/**
* Checks if a value is a function.
*
* @param value The value to check.
* @return Returns true if the value is a function, otherwise false.
*/
export default function isFunction(value: unknown): value is (...args: any[]) => any {
return typeof value === 'function';
}
/**
* Checks if a value is an integer.
*
* @param value - The value to check.
* @return Returns true if the value is an integer, otherwise returns false.
*/
export default function isInteger(value: unknown): value is number {
return typeof value === 'number' && Number.isInteger(value);
}
/**
* Determines if the given object is null or undefined.
*
* @param obj - The object to check.
* @returns True if the object is null or undefined, false otherwise.
*/
export default function isNil<T>(value: T): value is Extract<T, null | undefined> {
return value == null;
}
/**
* Checks if the given object is not null or undefined.
*
* @param obj - The object to be checked.
* @return Returns true if the object is not null or undefined, false otherwise.
*/
export default function isNonNil<T>(value: T): value is NonNullable<T> {
return value != null;
}
/**
* Checks if a value is a number.
*
* @param value - The value to check.
* @return Returns true if the value is a number, otherwise returns false.
*/
export default function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
/**
* Checks if the given value is a `Record`.
*
* A `Record` is a plain object with string, number or symbol key.
*
* @param value - The value to be checked.
* @return Returns true if the value is a record, false otherwise.
*/
export default function isRecord(
value: unknown,
): value is Record<keyof any, any> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
// credits to the author of lodash's isPlainObject function for
// the following part of the implementation
if (Object.getPrototypeOf(value) === null) {
return true;
}
let prototype = value;
while (Object.getPrototypeOf(prototype) !== null) {
prototype = Object.getPrototypeOf(prototype);
}
return Object.getPrototypeOf(value) === prototype;
}
/**
* Checks if a value is a string.
*
* @param value - The value to check.
* @return Returns true if the value is a string, otherwise returns false.
*/
export default function isString(value: unknown): value is string {
return typeof value === 'string';
}
type Comparator<T> = (item: T, other: T) => number;
/**
* Calculates the maximum value in an array by applying a mapper function to
* each element and comparing the results using a custom comparator function.
*
* @param array The array of elements to search.
* @param mapper The function to apply to each element in the array.
* @param comparator The function used to compare the results of the mapper
* function.
* @returns The maximum element in the array, or undefined if the array is
* empty.
*/
export default function maxByWith<T, U>(
array: T[],
mapper: (item: T) => U,
comparator: Comparator<U>,
): T | undefined {
if (!array.length) return undefined;
return array.reduce((prev, curr) => {
return comparator(mapper(prev), mapper(curr)) > 0 ? prev : curr;
}, array[0]);
}
type Comparator<T> = (item: T, other: T) => number;
/**
* Calculates the maximum value in an array using a custom comparator function.
*
* @param array The array of elements to search.
* @param comparator The function used to compare the results of the mapper
* function.
* @returns The maximum element in the array, or undefined if the array is
* empty.
*/
export default function maxWith<T>(
array: T[],
comparator: Comparator<T>,
): T | undefined {
if (!array.length) return undefined;
return array.reduce((prev, curr) => {
return comparator(prev, curr) > 0 ? prev : curr;
}, array[0]);
}
/**
* Moves an item within an array from one index to another. Mutates the passed
* array.
*
* @param array The array to modify.
* @param from The index of the item to move.
* @param to The index to move the item to.
* @return The modified array.
*/
export default function move<T>(array: T[], from: number, to: number) {
const toIndex = to < 0 ? array.length - to : to;
const item = array.splice(from, 1)[0];
array.splice(toIndex, 0, item);
return array;
}
import move from './move';
/**
* Moves an element within an array by a specified offset. Mutates the passed
* array.
*
* @param array The array to modify.
* @param index The index of the element to move.
* @param offset The offset by which to move the element.
* @return The modified array with the element moved.
*/
export default function moveBy<T>(array: T[], index: number, offset: number) {
if (offset === 0) return array;
const toIndex = Math.max(index + offset, 0);
return move(array, index, toIndex);
}
/**
* Omits the specified keys from an object and returns a new object without
* those keys.
*
* @param obj - The object to omit keys from.
* @param keys - The keys to omit from the object.
* @returns A new object with only the picked keys.
*/
export default function omit<
T extends Record<string, unknown>,
K extends keyof T,
>(obj: T, keys: K[]): Omit<T, K> {
return keys.reduce(
(acc, key) => {
delete acc[key];
return acc;
},
{ ...obj },
);
}
/**
* Picks the specified keys from an object and returns a new object with only
* those keys.
*
* @param obj - The object to pick keys from.
* @param keys - The keys to pick from the object.
* @returns A new object with only the picked keys.
*/
export default function pick<
T extends Record<string, unknown>,
K extends keyof T,
>(obj: T, keys: K[]): Pick<T, K> {
return keys.reduce(
(acc, key) => {
acc[key] = obj[key];
return acc;
},
{} as Pick<T, K>,
);
}
type Equatable<T> = (value: T, other: T) => boolean;
const DEFAULT_EQUATABLE: Equatable<unknown> = (a, b) => a === b;
/**
* Remove the first occurrence of a value from an array. Mutates the passed
* array.
*
* @param list - The array from which the value will be removed.
* @param value - The value to be removed.
* @param isEqual - The equality function used to compare values. Defaults to
* the default equality function.
* @return The modified array with the first occurrence of the value removed.
*/
export default function pull<T>(
list: T[],
value: T,
isEqual: Equatable<T> = DEFAULT_EQUATABLE,
) {
const index = list.findIndex((item) => isEqual(item, value));
if (index < 0) return list;
list.splice(index, 1);
return list;
}
type Predicate<T> = (value: T, index: number, list: T[]) => boolean;
/**
* Filters elements from the given list that do not satisfy the provided
* predicate function.
*
* @param list - The list to filter.
* @param predicate - The predicate function used to determine if an element
* should be filtered.
* @return The filtered list.
*/
export default function reject<T>(list: T[], predicate: Predicate<T>) {
return list.filter((value, index, array) => !predicate(value, index, array));
}
type Equatable<T> = (value: T, other: T) => boolean;
const DEFAULT_EQUATABLE: Equatable<unknown> = (a, b) => a === b;
/**
* Removes all occurrences of a specified value from an array. The returned
* array is a shallow copy of the original array.
*
* @param list - The array from which to remove the value.
* @param value - The value to be removed from the array.
* @param isEqual - (optional) A function that determines whether two values are
* equal. If not provided, a default equality function will be used.
* @return A new array with all occurrences of the specified value removed.
*/
export default function remove<T>(
list: T[],
value: T,
isEqual: Equatable<T> = DEFAULT_EQUATABLE,
) {
return list.filter((item) => !isEqual(item, value));
}
type Comparator<T> = (item: T, other: T) => number;
type SortOrder = 'asc' | 'desc';
const DEFAULT_ORDER: SortOrder = 'asc';
/**
* Sorts an array. Mutates the passed array.
*
* @param list - The array to be sorted.
* @param comparator - (Optional) A function that defines the sort order.
* @return The sorted array.
*/
export function sort<T>(
list: T[],
comparator?: Comparator<T>,
order: SortOrder = DEFAULT_ORDER,
) {
const c = order === 'asc' ? 1 : -1;
return list.sort((a, b) => comparator(a, b) * c);
}
/**
* Sorts an array using a custom sorting function. Mutates the passed array.
*
* @param list - The array to be sorted.
* @param with - The sorting function.
* @param order - The order of sorting.
* @returns - The sorted array.
*/
export function sortWith<T, U>(
list: T[],
fn: (item: T) => U,
order = DEFAULT_ORDER,
) {
return sort(list, (a, b) => (fn(a) > fn(b) ? f : -f), order);
}
/**
* Sorts an array of objects by a specified key. Mutates the passed array.
*
* @param list - The array of objects to be sorted.
* @param key - The key to sort the objects by.
* @param order - The order of sorting.
* @return The sorted array of objects.
*/
export function sortBy<T extends Record<string, unknown>>(
list: T[],
key: keyof T,
order = DEFAULT_ORDER,
) {
return sortWith(list, (item) => item[key], order);
}
function calcIndex(index: number, maxIndex: number) {
let expandedIndex = index;
if (index < 0) {
expandedIndex = maxIndex + 1 - index;
}
return Math.max(0, Math.min(expandedIndex, maxIndex));
}
/**
* Swaps the elements in an array at the specified old and new indices. Mutates
* the passed array.
*
* @param array The array to perform the swap operation on.
* @param from The index of the element to be swapped.
* @param to The index where the element will be moved to.
* @returns The modified array with the elements swapped.
*/
export default function swap<T>(array: T[], from: number, to: number): T[] {
const maxIndex = array.length - 1;
const fromIndex = calcIndex(from, maxIndex);
const toIndex = calcIndex(to, maxIndex);
if (fromIndex === toIndex) return array;
const sourceItem = array[fromIndex];
const targetItem = array[toIndex];
array[toIndex] = sourceItem;
array[fromIndex] = targetItem;
return array;
}
import isFunction from './isFunction';
type Predicate<T> = (value: T, index: number, array: T[]) => boolean;
/**
* Updates an array by applying an updater function to each element that satisfies the given predicate.
*
* @param array The array to be updated.
* @param where The predicate function used to find the element to be updated.
* @param set The function used to update the element.
* @returns The updated array.
*/
export default function update<T>(
array: T[],
where: Predicate<T>,
set: T | ((value: T, index: number, array: T[]) => T),
) {
array.forEach((value, index) => {
if (!where(value, index, array)) return;
array[index] = isFunction(set) ? set(value, index, array) : set;
});
return array;
}
/**
* Wraps the given value in an array if it is not already an array.
*
* @param value - The value to be wrapped.
* @returns The wrapped value.
*/
export default function wrapArray<T>(value: T | T[] | null | undefined): T[] {
if (value == null) return [];
if (Array.isArray(value)) return value;
return [value];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment