diff --git a/src/utils/__tests__/unit/dates.utils.test.ts b/src/utils/__tests__/unit/dates.utils.test.ts new file mode 100644 index 0000000..1c17c07 --- /dev/null +++ b/src/utils/__tests__/unit/dates.utils.test.ts @@ -0,0 +1,37 @@ +import { stringNumberOrDateToUNIXTimestamp } from "../../dates.utils"; + +describe("Dates utils", () => { + describe("stringNumberOrDateToUNIXTimestamp", () => { + const testDate = new Date("26 October 1985 01:20 PDT"); + const expectedValue = Math.round(testDate.valueOf() / 1000); + + it("should handle Date as Date", () => { + const unixTimestamp = stringNumberOrDateToUNIXTimestamp(testDate); + expect(unixTimestamp).toBe(expectedValue); + }); + + it("should handle Date as string", () => { + const unixTimestamp = stringNumberOrDateToUNIXTimestamp(testDate.toString()); + expect(unixTimestamp).toBe(expectedValue); + }); + + it("should handle Date as isostring", () => { + const unixTimestamp = stringNumberOrDateToUNIXTimestamp(testDate.toISOString()); + expect(unixTimestamp).toBe(expectedValue); + }); + + it("should handle timestamp as number", () => { + const numberTimestamp = testDate.valueOf(); + + const unixTimestamp = stringNumberOrDateToUNIXTimestamp(numberTimestamp); + expect(unixTimestamp).toBe(expectedValue); + }); + + it("should handle timestamp as string", () => { + const stringTimestamp = testDate.valueOf().toString(); + + const unixTimestamp = stringNumberOrDateToUNIXTimestamp(stringTimestamp); + expect(unixTimestamp).toBe(expectedValue); + }); + }); +}); diff --git a/src/utils/array.utils.ts b/src/utils/array.utils.ts new file mode 100644 index 0000000..5fb93fa --- /dev/null +++ b/src/utils/array.utils.ts @@ -0,0 +1,86 @@ +export async function asyncMap( + array: readonly T[], + callbackfn: (value: T, index: number, innerArray: readonly T[]) => Promise, + parallelized = false +): Promise { + if (parallelized) { + return Promise.all(array.map(callbackfn)); + } + const newArray = new Array(); + const arrayEntriesIterator = array.entries(); + for (const [index, value] of arrayEntriesIterator) { + newArray[index] = await callbackfn(value, index, array); + } + return newArray; +} + +export async function asyncForEach( + array: readonly T[], + callbackfn: (value: T, index: number, innerArray: readonly T[]) => Promise, + parallelized = false +): Promise { + if (parallelized) { + await Promise.all(array.map(callbackfn)); + } else { + const arrayEntriesIterator = array.entries(); + for (const [index, value] of arrayEntriesIterator) { + await callbackfn(value, index, array); + } + } +} + +export async function asyncReduce( + array: readonly T[], + callbackfn: ( + previousValue: U, + currentValue: T, + currentIndex: number, + innerArray: readonly T[] + ) => Promise, + initialValue: U +): Promise { + const arrayEntriesIterator = array.entries(); + for (const [index, value] of arrayEntriesIterator) { + initialValue = await callbackfn(initialValue, value, index, array); + } + return initialValue; +} + +export async function asyncFind( + array: readonly T[], + callbackfn: (value: T, index: number, innerArray: readonly T[]) => Promise +): Promise { + const arrayEntriesIterator = array.entries(); + for (const [index, value] of arrayEntriesIterator) { + if (await callbackfn(value, index, array)) { + return value; + } + } + return undefined; +} + +export async function asyncEvery( + array: readonly T[], + predicate: (value: T, index: number, array: readonly T[]) => Promise +): Promise { + const arrayEntriesIterator = array.entries(); + for (const [index, value] of arrayEntriesIterator) { + if (!(await predicate(value, index, array))) { + return false; + } + } + return true; +} + +export async function asyncSome( + array: readonly T[], + predicate: (value: T, index: number, array: readonly T[]) => Promise +): Promise { + const arrayEntriesIterator = array.entries(); + for (const [index, value] of arrayEntriesIterator) { + if (await predicate(value, index, array)) { + return true; + } + } + return false; +} diff --git a/src/utils/boolean.utils.ts b/src/utils/boolean.utils.ts new file mode 100644 index 0000000..5221d78 --- /dev/null +++ b/src/utils/boolean.utils.ts @@ -0,0 +1,5 @@ +import { randomSelect } from "./data.utils"; + +export function randomBoolean(): boolean { + return randomSelect(true, false); +} diff --git a/src/utils/country.utils.ts b/src/utils/country.utils.ts new file mode 100644 index 0000000..2cec3cc --- /dev/null +++ b/src/utils/country.utils.ts @@ -0,0 +1,255 @@ +const isoCountries = { + AF: "Afghanistan", + AX: "Aland Islands", + AL: "Albania", + DZ: "Algeria", + AS: "American Samoa", + AD: "Andorra", + AO: "Angola", + AI: "Anguilla", + AQ: "Antarctica", + AG: "Antigua And Barbuda", + AR: "Argentina", + AM: "Armenia", + AW: "Aruba", + AU: "Australia", + AT: "Austria", + AZ: "Azerbaijan", + BS: "Bahamas", + BH: "Bahrain", + BD: "Bangladesh", + BB: "Barbados", + BY: "Belarus", + BE: "Belgium", + BZ: "Belize", + BJ: "Benin", + BM: "Bermuda", + BT: "Bhutan", + BO: "Bolivia", + BA: "Bosnia And Herzegovina", + BW: "Botswana", + BV: "Bouvet Island", + BR: "Brazil", + IO: "British Indian Ocean Territory", + BN: "Brunei Darussalam", + BG: "Bulgaria", + BF: "Burkina Faso", + BI: "Burundi", + KH: "Cambodia", + CM: "Cameroon", + CA: "Canada", + CV: "Cape Verde", + KY: "Cayman Islands", + CF: "Central African Republic", + TD: "Chad", + CL: "Chile", + CN: "China", + CX: "Christmas Island", + CC: "Cocos (Keeling) Islands", + CO: "Colombia", + KM: "Comoros", + CG: "Congo", + CD: "Congo, Democratic Republic", + CK: "Cook Islands", + CR: "Costa Rica", + CI: "Cote D'Ivoire", + HR: "Croatia", + CU: "Cuba", + CY: "Cyprus", + CZ: "Czech Republic", + DK: "Denmark", + DJ: "Djibouti", + DM: "Dominica", + DO: "Dominican Republic", + EC: "Ecuador", + EG: "Egypt", + SV: "El Salvador", + GQ: "Equatorial Guinea", + ER: "Eritrea", + EE: "Estonia", + ET: "Ethiopia", + FK: "Falkland Islands (Malvinas)", + FO: "Faroe Islands", + FJ: "Fiji", + FI: "Finland", + FR: "France", + GF: "French Guiana", + PF: "French Polynesia", + TF: "French Southern Territories", + GA: "Gabon", + GM: "Gambia", + GE: "Georgia", + DE: "Germany", + GH: "Ghana", + GI: "Gibraltar", + GR: "Greece", + GL: "Greenland", + GD: "Grenada", + GP: "Guadeloupe", + GU: "Guam", + GT: "Guatemala", + GG: "Guernsey", + GN: "Guinea", + GW: "Guinea-Bissau", + GY: "Guyana", + HT: "Haiti", + HM: "Heard Island & Mcdonald Islands", + VA: "Holy See (Vatican City State)", + HN: "Honduras", + HK: "Hong Kong", + HU: "Hungary", + IS: "Iceland", + IN: "India", + ID: "Indonesia", + IR: "Iran, Islamic Republic Of", + IQ: "Iraq", + IE: "Ireland", + IM: "Isle Of Man", + IL: "Israel", + IT: "Italy", + JM: "Jamaica", + JP: "Japan", + JE: "Jersey", + JO: "Jordan", + KZ: "Kazakhstan", + KE: "Kenya", + KI: "Kiribati", + KR: "Korea", + KW: "Kuwait", + KG: "Kyrgyzstan", + LA: "Lao People's Democratic Republic", + LV: "Latvia", + LB: "Lebanon", + LS: "Lesotho", + LR: "Liberia", + LY: "Libyan Arab Jamahiriya", + LI: "Liechtenstein", + LT: "Lithuania", + LU: "Luxembourg", + MO: "Macao", + MK: "Macedonia", + MG: "Madagascar", + MW: "Malawi", + MY: "Malaysia", + MV: "Maldives", + ML: "Mali", + MT: "Malta", + MH: "Marshall Islands", + MQ: "Martinique", + MR: "Mauritania", + MU: "Mauritius", + YT: "Mayotte", + MX: "Mexico", + FM: "Micronesia, Federated States Of", + MD: "Moldova", + MC: "Monaco", + MN: "Mongolia", + ME: "Montenegro", + MS: "Montserrat", + MA: "Morocco", + MZ: "Mozambique", + MM: "Myanmar", + NA: "Namibia", + NR: "Nauru", + NP: "Nepal", + NL: "Netherlands", + AN: "Netherlands Antilles", + NC: "New Caledonia", + NZ: "New Zealand", + NI: "Nicaragua", + NE: "Niger", + NG: "Nigeria", + NU: "Niue", + NF: "Norfolk Island", + MP: "Northern Mariana Islands", + NO: "Norway", + OM: "Oman", + PK: "Pakistan", + PW: "Palau", + PS: "Palestinian Territory, Occupied", + PA: "Panama", + PG: "Papua New Guinea", + PY: "Paraguay", + PE: "Peru", + PH: "Philippines", + PN: "Pitcairn", + PL: "Poland", + PT: "Portugal", + PR: "Puerto Rico", + QA: "Qatar", + RE: "Reunion", + RO: "Romania", + RU: "Russian Federation", + RW: "Rwanda", + BL: "Saint Barthelemy", + SH: "Saint Helena", + KN: "Saint Kitts And Nevis", + LC: "Saint Lucia", + MF: "Saint Martin", + PM: "Saint Pierre And Miquelon", + VC: "Saint Vincent And Grenadines", + WS: "Samoa", + SM: "San Marino", + ST: "Sao Tome And Principe", + SA: "Saudi Arabia", + SN: "Senegal", + RS: "Serbia", + SC: "Seychelles", + SL: "Sierra Leone", + SG: "Singapore", + SK: "Slovakia", + SI: "Slovenia", + SB: "Solomon Islands", + SO: "Somalia", + ZA: "South Africa", + GS: "South Georgia And Sandwich Isl.", + ES: "Spain", + LK: "Sri Lanka", + SD: "Sudan", + SR: "Suriname", + SJ: "Svalbard And Jan Mayen", + SZ: "Swaziland", + SE: "Sweden", + CH: "Switzerland", + SY: "Syrian Arab Republic", + TW: "Taiwan", + TJ: "Tajikistan", + TZ: "Tanzania", + TH: "Thailand", + TL: "Timor-Leste", + TG: "Togo", + TK: "Tokelau", + TO: "Tonga", + TT: "Trinidad And Tobago", + TN: "Tunisia", + TR: "Turkey", + TM: "Turkmenistan", + TC: "Turks And Caicos Islands", + TV: "Tuvalu", + UG: "Uganda", + UA: "Ukraine", + AE: "United Arab Emirates", + GB: "United Kingdom", + US: "United States", + UM: "United States Outlying Islands", + UY: "Uruguay", + UZ: "Uzbekistan", + VU: "Vanuatu", + VE: "Venezuela", + VN: "Viet Nam", + VG: "Virgin Islands, British", + VI: "Virgin Islands, U.S.", + WF: "Wallis And Futuna", + EH: "Western Sahara", + YE: "Yemen", + ZM: "Zambia", + ZW: "Zimbabwe", +} as const; + +export function getCountryName(countryCode: keyof typeof isoCountries): string { + const cc = countryCode.toUpperCase(); + if (cc in isoCountries) { + return isoCountries[countryCode]; + } + return cc; +} diff --git a/src/utils/data.utils.ts b/src/utils/data.utils.ts new file mode 100644 index 0000000..4694bbf --- /dev/null +++ b/src/utils/data.utils.ts @@ -0,0 +1,16 @@ +import { randomString } from "./string.utils"; + +export function randomEmailAddress(): string { + return `${randomString(8, { numbers: false, uppers: false })}@${randomString(5, { + numbers: false, + uppers: false, + })}.com`; +} + +export function randomSelect(...dataset: Array): T { + return dataset[Math.round(Math.random() * (dataset.length - 1))]!; +} + +export function randomId(): number { + return Math.round(Math.random() * (2 ** 32 - 1)); +} diff --git a/src/utils/dates.utils.ts b/src/utils/dates.utils.ts new file mode 100644 index 0000000..ae57546 --- /dev/null +++ b/src/utils/dates.utils.ts @@ -0,0 +1,25 @@ +import { isDate } from "util/types"; + +const invalidDateError = "Invalid Date" as const; +export function stringNumberOrDateToUNIXTimestamp(arg: Date | number | string): number { + let parsedDate = isDate(arg) ? arg : (new Date(arg) as Date | typeof invalidDateError); + if (parsedDate === invalidDateError || isNaN(parsedDate.valueOf())) { + parsedDate = new Date(parseInt(arg as string, 10)) as + | Date + | typeof invalidDateError; + if (parsedDate === invalidDateError || isNaN(parsedDate.valueOf())) { + throw new TypeError( + "Passed argument is neither a valid date nor date string nor timestamp" + ); + } + } + return Math.round(parsedDate.valueOf() / 1000); +} + +export function getSmallestDate(): Date { + return new Date(-53690); +} + +export function getGreatestDate(): Date { + return new Date(253402300799997); +} diff --git a/src/utils/file-system.utils.ts b/src/utils/file-system.utils.ts new file mode 100644 index 0000000..9aa4980 --- /dev/null +++ b/src/utils/file-system.utils.ts @@ -0,0 +1,11 @@ +export const MimeMap = Object.freeze({ + pdf: "application/pdf", + csv: "text/csv", + zip: "application/gzip", + csv_gz: "application/gzip", + xls: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", +} as const); + +export type FileExt = keyof typeof MimeMap; +export type MIME = (typeof MimeMap)[FileExt]; diff --git a/src/utils/fp.utils.ts b/src/utils/fp.utils.ts new file mode 100644 index 0000000..1b084a1 --- /dev/null +++ b/src/utils/fp.utils.ts @@ -0,0 +1,67 @@ +import type { ClassInstance, GenericClass } from "./types"; + +export const isFalsy = (arg: T): boolean => !arg; +export const isTruthy = (arg: T): boolean => !!arg; + +export const throwError = (error?: Error) => { + throw error ?? new Error("Undesirable issue"); +}; + +export const nullary = + (f: (...args: T[]) => U): (() => U) => + () => + f(); + +export const unary = + , U>(f: (...args: [...T]) => U): ((arg: T[0]) => U) => + (arg: T[0]) => + (f as unknown as (innerArg: T[0]) => U)(arg); + +export const rejector = ((...args: [Error | string, ...unknown[]]) => { + switch (args.length) { + case 1: + return () => Promise.reject(args[0]); + case 0: + return (arg: unknown) => Promise.reject(arg); + default: + throw new Error("instanceOf called with invalid number of arguments"); + } +}) as (() => (error: Error | string) => Promise) & + ((error: Error | string) => () => Promise); + +export const resolver = ((...args: unknown[]) => { + switch (args.length) { + case 1: + return () => Promise.resolve(args[0]); + case 0: + return (arg: unknown) => Promise.resolve(arg); + default: + throw new Error("instanceOf called with invalid number of arguments"); + } +}) as (() => (error: Error | string) => Promise) & + ((error: Error | string) => () => Promise); + +export const nothing = (): undefined => undefined; + +export const empty = (): void => undefined; + +export const instanceOf = ((...args: [GenericClass, ...unknown[]]) => { + switch (args.length) { + case 2: + return args[1] instanceof args[0]; + case 1: + return (innerArg: unknown) => innerArg instanceof args[0]; + default: + throw new Error("instanceOf called with invalid number of arguments"); + } +}) as (>( + clazz: C, + arg: unknown +) => arg is I) & + (>( + clazz: C + ) => (arg: unknown) => arg is I); + +export const isNil = (arg: unknown): arg is null | undefined => { + return arg === null || arg === undefined; +}; diff --git a/src/utils/map.utils.ts b/src/utils/map.utils.ts new file mode 100644 index 0000000..8976c8a --- /dev/null +++ b/src/utils/map.utils.ts @@ -0,0 +1,17 @@ +export function mapMerge(...maps: Array>): Map { + return maps.reduce((acc, map) => { + for (const [key, value] of map) { + acc.set(key, value); + } + return acc; + }, new Map()); +} + +export function mapConcat(...maps: Array>): Map { + return maps.reduce((acc, map) => { + for (const [key, value] of map) { + acc.set(key, acc.has(key) ? acc.get(key)!.concat(value) : [value]); + } + return acc; + }, new Map()); +} diff --git a/src/utils/nodejs.utils.ts b/src/utils/nodejs.utils.ts new file mode 100644 index 0000000..a79c4e3 --- /dev/null +++ b/src/utils/nodejs.utils.ts @@ -0,0 +1,23 @@ +export function isReadableStream(arg: unknown): arg is NodeJS.ReadStream { + return ( + typeof (arg as NodeJS.ReadableStream)?.readable === "boolean" && + typeof (arg as NodeJS.ReadableStream)?.read === "function" + ); +} + +export function isWritableStream(arg: unknown): arg is NodeJS.WriteStream { + return ( + typeof (arg as NodeJS.WritableStream)?.writable === "boolean" && + typeof (arg as NodeJS.WritableStream)?.write === "function" + ); +} + +export function isReadWriteStream(arg: unknown): arg is NodeJS.ReadWriteStream { + return isReadableStream(arg) && isWritableStream(arg); +} + +export function isStream( + arg: unknown +): arg is NodeJS.ReadableStream | NodeJS.WritableStream | NodeJS.ReadWriteStream { + return isReadableStream(arg) || isWritableStream(arg); +} diff --git a/src/utils/number.utils.ts b/src/utils/number.utils.ts new file mode 100644 index 0000000..94c745b --- /dev/null +++ b/src/utils/number.utils.ts @@ -0,0 +1,25 @@ +import { randomSelect } from "./data.utils"; + +export function randomPositiveInteger(factor = 10): number { + return Math.round(Math.random() * factor); +} + +export function randomPositiveNumber(factor = 1000): number { + return Math.round(Math.random() * factor); +} + +export function randomNumber(factor = 1000): number { + return randomPositiveNumber(factor) * randomSelect(1, -1); +} + +export function randomNumberInRange(min: number = 0, max: number = 100): number { + if (min > max) { + throw new Error("Args make no sense"); + } + if (min === max) { + return min; + } + const delta = Math.abs(max - min); + const sign = min < 0 && max < 0 ? -1 : min < 0 && max > 0 ? randomSelect(1, -1) : 1; + return Math.max(min, Math.min(max, (randomPositiveNumber(delta) + min) * sign)); +} diff --git a/src/utils/object.utils.ts b/src/utils/object.utils.ts new file mode 100644 index 0000000..c8173ba --- /dev/null +++ b/src/utils/object.utils.ts @@ -0,0 +1,212 @@ +import type { + GenericRecord, + Primitive, + StripNil, + StripNull, + StripUndefined, + Union, +} from "./types"; + +export function keyof(obj: T): Array { + return Object.getOwnPropertyNames(obj) + .reduce((acc, key) => { + const numberKey = parseFloat(key) as Extract; + if (!Number.isNaN(numberKey) && numberKey.toString() === key) { + acc.push(numberKey); + } else { + acc.push(key as keyof T); + } + return acc; + }, new Array()) + .concat(Object.getOwnPropertySymbols(obj) as Array); +} + +export function ownEntries( + obj: T +): Array { + return keyof(obj).map(prop => [prop, obj[prop]] as const); +} + +export function transform< + T extends { [P in keyof T]: T[P] }, + U extends { [P in keyof U]: U[P] } +>( + obj: T, + cb: ( + args: readonly [key: V, value: T[V]], + index: number, + array: Array<{ [P in keyof T]: readonly [P, T[P]] }[keyof T]> + ) => readonly [W, U[W]] +): U { + return Object.fromEntries( + ( + Object.entries(obj) as unknown as Array< + { [P in keyof T]: readonly [P, T[P]] }[keyof T] + > + ).map(cb) + ) as U; +} + +/** + * @description This behaves like JSON.stringify with the exception that it works with recursive objects + * while also respecting JSON standards by replacing self-references by {$ref} objects + */ +export function stringify( + arg: unknown | undefined | null, + options: { + maxArrayLength?: number; + maxObjectPropertiesCount?: number; + maxStringLength?: number; + maxDepth?: number; + truncateLineBreaks?: boolean; + includeFunctionNames?: boolean; + } = {} +): string { + const references = new Map(); + const stringifyPrimitiveOrDate = (prim: Primitive | Date): string => { + let str = + prim === undefined + ? "undefined" + : prim === null + ? "null" + : (prim as { toJSON?: () => string })?.toJSON?.() ?? + (typeof prim === "string" + ? `"${prim.replace(/"/g, "'")}"` + : prim?.toString?.()) ?? + "unknown"; + if ( + typeof options.maxStringLength === "number" && + options.maxStringLength > 0 && + str.length > options.maxStringLength + ) { + str = `${str.slice(0, options.maxStringLength)}[...]"`; + } + if (options.truncateLineBreaks) { + str = str.replace(/\n/g, "\\n"); + } + return str; + }; + const stringifyObject = ( + obj: Record, + padding = "", + path = "#", + depth = 0 + ): string => { + const isArray = Array.isArray(obj); + const openerChar = isArray ? "[" : "{"; + const closerChar = isArray ? "]" : "}"; + references.set(obj, path); + const untruncatedEntries = options.includeFunctionNames + ? isArray + ? Object.entries(obj) + : ownEntries(obj) + : (isArray ? Object.entries(obj) : ownEntries(obj)).filter( + ([_, value]) => typeof value !== "function" + ); + const maxEntriesLength = isArray + ? options.maxArrayLength + : options.maxObjectPropertiesCount; + const [entries, isTruncated] = + typeof maxEntriesLength === "number" && + maxEntriesLength > 0 && + untruncatedEntries.length > maxEntriesLength + ? [untruncatedEntries.slice(0, maxEntriesLength), true] + : [untruncatedEntries, false]; + const str = `${openerChar}${ + entries.length > 0 + ? entries.reduce( + (acc, [key, value], index, arr) => + [ + acc, + `${`${padding} `}${isArray ? "" : `"${String(key)}": `}${ + options.includeFunctionNames && + typeof value === "function" + ? `"${value.name}"` + : typeof value === "object" && value !== null + ? references.has(value) + ? `{ "$ref": "${references.get(value)!}" }` + : stringifyObject( + value as Record, + `${padding} `, + `${path}/${String(key)}`, + depth + 1 + ) + : stringifyPrimitiveOrDate( + value as Record + ) + }${index === arr.length - 1 ? "" : ","}`, + ].join("\n"), + "" + ) + + (isTruncated + ? `,\n${padding} ...(${( + untruncatedEntries.length - entries.length + ).toString()} remaining elements)...\n${padding}` + : `\n${padding}`) + : "" + }${closerChar}`; + if (typeof options.maxDepth === "number" && options.maxDepth > 0) { + if (depth === options.maxDepth) { + return `${openerChar} ...(Max object depth setting (${options.maxDepth}) reached)... ${closerChar}`; + } + if (depth > options.maxDepth) { + return ""; + } + } + return str; + }; + return ( + typeof arg === "object" && arg !== null ? stringifyObject : stringifyPrimitiveOrDate + )(arg as Record); +} + +/** + * @description Removes from obj the properties whose names are not in keys + */ +export function pick>( + obj: T, + ...keys: K +) { + return Object.fromEntries( + ownEntries(obj).filter(([key, _]) => keys.includes(key)) + ) as unknown as { + [P in Extract>]: T[P]; + }; +} + +/** + * @description Removes from obj the properties whose names are in keys + */ +export function omit>( + obj: T, + ...keys: K +) { + return Object.fromEntries( + ownEntries(obj).filter(([key, _]) => !keys.includes(key)) + ) as unknown as { + [P in Exclude>]: T[P]; + }; +} + +/** + * @description Removes from obj properties which value is undefined + */ +export function stripUndefined(obj: T): StripUndefined { + return Object.fromEntries(ownEntries(obj).filter(([_, val]) => val !== undefined)) as T; +} + +/** + * @description Removes from obj properties which value is null + */ +export function stripNull(obj: T): StripNull { + return Object.fromEntries(ownEntries(obj).filter(([_, val]) => val !== null)) as T; +} + +/** + * @description Removes from obj properties which value is nil + */ +export function stripNil(obj: T): StripNil { + return Object.fromEntries( + ownEntries(obj).filter(([_, val]) => val !== null && val !== undefined) + ) as T; +} diff --git a/src/utils/regex.utils.ts b/src/utils/regex.utils.ts new file mode 100644 index 0000000..b111d6d --- /dev/null +++ b/src/utils/regex.utils.ts @@ -0,0 +1,2 @@ +export const noEmojisRegExp = + /^[\t\n\r\v\x20-\u2300\u27C0-\u2B00\u2C00-\u{1F000}\u{1FB00}-\u{E0000}]+$/u; diff --git a/src/utils/string.utils.ts b/src/utils/string.utils.ts new file mode 100644 index 0000000..5d1b97e --- /dev/null +++ b/src/utils/string.utils.ts @@ -0,0 +1,37 @@ +export function randomString( + length: number = 32, + flags: Partial<{ + specials: boolean; + punctuation: boolean; + spaces: boolean; + numbers: boolean; + lowers: boolean; + uppers: boolean; + numbersSet: string; + lowersSet: string; + uppersSet: string; + punctuationSet: string; + specialsSet: string; + }> = {} +): string { + flags = { + numbers: true, + lowers: true, + uppers: true, + ...flags, + }; + if (Object.getOwnPropertyNames(flags).length === 0 || length <= 0) { + throw new Error("Please provide at least one flag"); + } + const chars = + (flags.punctuation ? flags.punctuationSet || "'!,.;:?" : "") + + (flags.specials ? flags.specialsSet || '-"#$%&()*+/<=>@[\\]^_`{|}~' : "") + + (flags.spaces ? " " : "") + + (flags.numbers ? flags.numbersSet || "0123456789" : "") + + (flags.lowers ? flags.lowersSet || "abcdefghijklmnopqrstuvwxyz" : "") + + (flags.uppers ? flags.uppersSet || "ABCDEFGHIJKLMNOPQRSTUVWXYZ" : ""); + return Array.from( + { length }, + () => chars[Math.round(Math.random() * (chars.length - 1))]! + ).join(""); +} diff --git a/src/utils/types/array.ts b/src/utils/types/array.ts new file mode 100644 index 0000000..ef81690 --- /dev/null +++ b/src/utils/types/array.ts @@ -0,0 +1,2 @@ +export type GenericArray = Array; +export type ArrayValue = T extends Iterable | ArrayLike ? U : never; diff --git a/src/utils/types/boolean.ts b/src/utils/types/boolean.ts new file mode 100644 index 0000000..abe6fbc --- /dev/null +++ b/src/utils/types/boolean.ts @@ -0,0 +1,4 @@ +import type { Primitive } from "./primitive"; + +export type falsy = false | 0 | "" | undefined | null; // Should include NaN but typeof NaN is number and we don't want to exclude all the numbers +export type truthy = Exclude; diff --git a/src/utils/types/class.ts b/src/utils/types/class.ts new file mode 100644 index 0000000..15b2aa9 --- /dev/null +++ b/src/utils/types/class.ts @@ -0,0 +1,18 @@ +import type { GenericFunction } from "./function"; + +export type GenericClass = new (...args: unknown[]) => unknown; +export type ClassInstance = T extends new (...args: unknown[]) => infer I ? I : never; + +type MethodsNames> = { + [K in keyof C]: C[K] extends GenericClass + ? K + : C[K] extends GenericFunction + ? K + : never; +}[keyof C]; + +export type ClassMethods> = Pick>; +export type ClassProperties> = Omit< + C, + MethodsNames +>; diff --git a/src/utils/types/function.ts b/src/utils/types/function.ts new file mode 100644 index 0000000..7457e13 --- /dev/null +++ b/src/utils/types/function.ts @@ -0,0 +1,4 @@ +export type GenericFunction = (...args: unknown[]) => unknown; + +export type ReturnTypeAsync Promise> = + T extends (...args: unknown[]) => Promise ? R : never; diff --git a/src/utils/types/index.ts b/src/utils/types/index.ts new file mode 100644 index 0000000..bea2828 --- /dev/null +++ b/src/utils/types/index.ts @@ -0,0 +1,8 @@ +export * from "./array"; +export * from "./boolean"; +export * from "./class"; +export * from "./function"; +export * from "./object"; +export * from "./primitive"; +export * from "./serialization"; +export * from "./tuple"; diff --git a/src/utils/types/object.ts b/src/utils/types/object.ts new file mode 100644 index 0000000..1176ae1 --- /dev/null +++ b/src/utils/types/object.ts @@ -0,0 +1,123 @@ +import type { falsy } from "./boolean"; + +export type GenericRecord = { [P in keyof T]: T[P] }; + +export type EmptyRecord = { [P in PropertyKey]: never }; + +export type StrictMap = { + [P in keyof T]: T[P] extends undefined ? never : Exclude; +}; + +export type PartiallyUndefined = { + [P in keyof T]?: T[P] | undefined; +}; + +export type RequiredExcept = { + [P in Extract]?: T[P]; +} & { + [P in Exclude]-?: T[P]; +}; + +export type PartialExcept = { + [P in Exclude]?: T[P]; +} & { + [P in Extract]-?: T[P]; +}; + +/** @description From T, pick a set of properties which types are in K */ +export type Grab = Pick< + T, + { + [P in keyof T]: T[P] extends U ? P : never; + }[keyof T] +>; + +/** @description Construct a type with the properties of T except for those which type is in type K. */ +export type Strip = Pick< + T, + { + [P in keyof T]: T[P] extends U ? never : P; + }[keyof T] +>; + +/** @description Construct a type with the properties of T except for those which are undefined. */ +export type StripUndefined = Pick< + T, + { + [P in keyof T]: T[P] extends undefined ? never : P; + }[keyof T] +>; + +/** @description Construct a type with the properties of T except for those which are null. */ +export type StripNull = Pick< + T, + { + [P in keyof T]: T[P] extends null ? never : P; + }[keyof T] +>; + +/** @description Construct a type with the properties of T except for those which are null or undefined. */ +export type StripNil = Pick< + T, + { + [P in keyof T]: T[P] extends null | undefined ? never : P; + }[keyof T] +>; + +type OptionalPropertiesNames = { + [P in keyof T]-?: { [A in P]?: T[P] } extends { [A in P]: T[P] } ? P : never; +}[keyof T]; + +export type GrabOptional = Pick>; +export type StripRequired = Omit< + T, + Exclude> +>; +export type GrabRequired = Pick< + T, + Exclude> +>; +export type StripOptional = Omit>; + +export type Optional = Omit & { + [P in K]?: T[P] | undefined; +}; + +export type Full = Exclude; + +/* + ! Experimental + */ +export type Conditional = T extends falsy ? V : U; +export type WhenNotUndefined = T extends undefined + ? V + : U; + +/** @description Allows to view a 'T' discriminated union when its type at index 'DiscriminantKey' is of 'DiscriminantValue' type */ +export type Constrained< + T extends GenericRecord, + DiscriminantKey extends keyof T, + DiscriminantValue extends T[DiscriminantKey] +> = T extends { [P in DiscriminantKey]: DiscriminantValue } ? T : never; + +/** @description Allows to genericly view a 'T' discriminated union when its type at index 'DiscriminantKey' is of 'DiscriminantValue' type */ +export type GenericlyConstrained< + T extends GenericRecord, + DiscriminantKey extends keyof T, + DiscriminantValue extends T[DiscriminantKey] +> = T[DiscriminantKey] extends DiscriminantValue ? T : never; + +/** @description Allows to view a 'T' discriminated union when its type at index 'DiscriminantKey' is all but 'DiscriminantValue' type */ +export type NotConstrained< + T extends GenericRecord, + DiscriminantKey extends keyof T, + DiscriminantValue extends T[DiscriminantKey] +> = T extends { [P in DiscriminantKey]: Exclude } + ? T + : never; + +export type GenericlyNotConstrained< + T extends GenericRecord, + DiscriminantKey extends keyof T, + DiscriminantValue extends T[DiscriminantKey] +> = T[DiscriminantKey] extends DiscriminantValue ? never : T; diff --git a/src/utils/types/primitive.ts b/src/utils/types/primitive.ts new file mode 100644 index 0000000..6e2cab9 --- /dev/null +++ b/src/utils/types/primitive.ts @@ -0,0 +1,20 @@ +export type Primitive = + | string + | number + | bigint + | boolean + | symbol + | undefined + | object + | null; +export type TypeOfValues = + | "string" + | "number" + | "bigint" + | "boolean" + | "symbol" + | "undefined" + | "object" + | "function"; + +export type Nullable = T extends null | undefined ? T : never; diff --git a/src/utils/types/serialization.ts b/src/utils/types/serialization.ts new file mode 100644 index 0000000..f754405 --- /dev/null +++ b/src/utils/types/serialization.ts @@ -0,0 +1,28 @@ +import type { GenericFunction } from "./function"; +import type { Grab, Strip } from "./object"; + +type Deserializable = string | number | boolean | object | null; + +/** + * @description This represents the resulting structure of JSON.parse(JSON.stringify(T)), + * which holds no functions, only basic objects, arrays and a subset of primitive values + * (string, number, bigint, boolean, null) + */ +export type Serialized = T extends GenericFunction + ? undefined + : T extends { toJSON: GenericFunction } + ? ReturnType + : T extends [infer U, ...infer S] + ? [ + U extends GenericFunction ? null : Serialized, + ...(S["length"] extends 0 ? [] : Serialized) + ] + : T extends Array + ? V extends GenericFunction + ? [null] + : Array> + : T extends object + ? { [P in keyof Grab, Deserializable>]: Serialized } + : T extends Deserializable + ? T + : never; diff --git a/src/utils/types/tuple.ts b/src/utils/types/tuple.ts new file mode 100644 index 0000000..8164f82 --- /dev/null +++ b/src/utils/types/tuple.ts @@ -0,0 +1,33 @@ +export type GenericTuple = readonly [unknown, ...unknown[]]; +export type Tuple = readonly [T, ...T[]]; + +export type PartialTuple = T extends [infer U, ...infer V] + ? (V["length"] extends 0 ? [U] : [U, ...PartialTuple] | [U]) | [] + : never; + +export type ReversePartialTuple = T extends [ + ...infer U, + infer V +] + ? (U["length"] extends 0 ? [V] : [...ReversePartialTuple, V] | [V]) | [] + : never; + +/** + * @description Turns a tuple into a union + * @example ['a', 'b', 'c'] => 'a' | 'b' | 'c' + */ +export type Union = T extends readonly [infer U, ...infer V] + ? V["length"] extends 0 + ? U + : U | Union + : never; + +/** + * @description Turns a tuple into an intersection + * @example ['a', 'b', 'c'] => 'a' & 'b' & 'c' + */ +export type Intersection = T extends readonly [infer U, ...infer V] + ? V["length"] extends 0 + ? U + : U & Intersection + : never; diff --git a/tsconfig.json b/tsconfig.json index 09dffd8..df6ff1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,7 +31,7 @@ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ "typeRoots": [ "./node_modules/@types", - "./src/types" + "src/utils/types" ] /* Specify multiple folders that act like `./node_modules/@types`. */, "types": [ "node",