Docs update
This commit is contained in:
55
src/array.ts
55
src/array.ts
@ -1,10 +1,32 @@
|
||||
import {dotNotation, isEqual} from './objects';
|
||||
|
||||
/**
|
||||
* Only add element to array if it isn't already included
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const arr = addUnique([1, 2, 3], 3);
|
||||
* console.log(arr); // Output: [1, 2, 3]
|
||||
* ```
|
||||
*
|
||||
* @param {T[]} array Target array element will be added to
|
||||
* @param {T} el Unique element to add
|
||||
* @return {T[]} Array with element if it was unique
|
||||
* @deprecated Use ASet to create unique arrays
|
||||
*/
|
||||
export function addUnique<T>(array: T[], el: T): T[] {
|
||||
if(array.indexOf(el) === -1) array.push(el);
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all unique elements in arrays
|
||||
*
|
||||
* @param {any[]} a First array to compare
|
||||
* @param {any[]} b Second array to compare
|
||||
* @return {any[]} Unique elements
|
||||
* @deprecated Use ASet to perform Set operations on arrays
|
||||
*/
|
||||
export function arrayDiff(a: any[], b: any[]): any[] {
|
||||
return makeUnique([
|
||||
...a.filter(v1 => !b.includes((v2: any) => isEqual(v1, v2))),
|
||||
@ -33,6 +55,25 @@ export function caseInsensitiveSort(prop: string) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand to find objects with a property value
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const found = [
|
||||
* {name: 'Batman'},
|
||||
* {name: 'Superman'},
|
||||
* ].filter(findByProp('name', 'Batman'));
|
||||
* ```
|
||||
*
|
||||
* @param {string} prop Property to compare (Dot nation supported)
|
||||
* @param value Value property must have
|
||||
* @return {(v: any) => boolean} Function used by `filter` or `find`
|
||||
*/
|
||||
export function findByProp(prop: string, value: any) {
|
||||
return (v: any) => isEqual(dotNotation(v, prop), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively flatten nested arrays
|
||||
*
|
||||
@ -91,10 +132,13 @@ export function sortByProp(prop: string, reverse = false) {
|
||||
};
|
||||
}
|
||||
|
||||
export function findByProp(prop: string, value: any) {
|
||||
return (v: any) => isEqual(v[prop], value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure every element in array is unique
|
||||
*
|
||||
* @param {any[]} arr Array that will be filtered in place
|
||||
* @return {any[]} Original array
|
||||
* @deprecated Please use ASet to create a guaranteed unique array
|
||||
*/
|
||||
export function makeUnique(arr: any[]) {
|
||||
for(let i = arr.length - 1; i >= 0; i--) {
|
||||
if(arr.slice(0, i).find(n => isEqual(n, arr[i]))) arr.splice(i, 1);
|
||||
@ -103,7 +147,8 @@ export function makeUnique(arr: any[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure value is an array, if it isn't wrap it in one.
|
||||
* Make sure value is an array, if it isn't wrap it in one
|
||||
*
|
||||
* @param {T[] | T} value Value that should be an array
|
||||
* @returns {T[]} Value in an array
|
||||
*/
|
||||
|
43
src/files.ts
43
src/files.ts
@ -1,21 +1,39 @@
|
||||
import {deepCopy, JSONAttemptParse} from './objects.ts';
|
||||
import {JSONAttemptParse} from './objects.ts';
|
||||
import {PromiseProgress} from './promise-progress';
|
||||
|
||||
export function download(href: any, name: string) {
|
||||
/**
|
||||
* Download a file from a URL
|
||||
*
|
||||
* @param href URL that will be downloaded
|
||||
* @param {string} name Override download name
|
||||
*/
|
||||
export function download(href: any, name?: string) {
|
||||
const a = document.createElement('a');
|
||||
a.href = href;
|
||||
a.download = name;
|
||||
a.download = name || href.split('/').pop();
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download blob as a file
|
||||
*
|
||||
* @param {Blob} blob File as a blob
|
||||
* @param {string} name Name blob will be downloaded as
|
||||
*/
|
||||
export function downloadBlob(blob: Blob, name: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
download(url, name);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open filebrowser & return selected file
|
||||
*
|
||||
* @param {{accept?: string, multiple?: boolean}} options accept - selectable mimetypes, multiple - Allow selecting more than 1 file
|
||||
* @return {Promise<File[]>} Array of selected files
|
||||
*/
|
||||
export function fileBrowser(options: {accept?: string, multiple?: boolean} = {}): Promise<File[]> {
|
||||
return new Promise(res => {
|
||||
const input = document.createElement('input');
|
||||
@ -32,6 +50,25 @@ export function fileBrowser(options: {accept?: string, multiple?: boolean} = {})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create timestamp intended for filenames from a date
|
||||
*
|
||||
* @param {string} name Name of file, `{{TIMESTAMP}}` will be replaced
|
||||
* @param {Date | number | string} date Date to use for timestamp
|
||||
* @return {string} Interpolated filename, or the raw timestamp if name was omitted
|
||||
*/
|
||||
export function timestampFilename(name?: string, date: Date | number | string = new Date()) {
|
||||
if(typeof date == 'number' || typeof date == 'string') date = new Date(date);
|
||||
const timestamp = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}_${date.getHours().toString().padStart(2, '0')}-${date.getMinutes().toString().padStart(2, '0')}-${date.getSeconds().toString().padStart(2, '0')}`;
|
||||
return name ? name.replace('{{TIMESTAMP}}', timestamp) : timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file to URL with progress callback using PromiseProgress
|
||||
*
|
||||
* @param {{url: string, files: File[], headers?: {[p: string]: string}, withCredentials?: boolean}} options
|
||||
* @return {PromiseProgress<T>} Promise of request with `onProgress` callback
|
||||
*/
|
||||
export function uploadWithProgress<T>(options: {
|
||||
url: string;
|
||||
files: File[];
|
||||
|
@ -11,3 +11,4 @@ export * from './objects';
|
||||
export * from './promise-progress';
|
||||
export * from './string';
|
||||
export * from './time';
|
||||
export * from './types';
|
||||
|
39
src/misc.ts
39
src/misc.ts
@ -11,42 +11,3 @@ export function gravatar(email: string, def='mp') {
|
||||
if(!email) return '';
|
||||
return `https://www.gravatar.com/avatar/${md5(email)}?d=${def}`;
|
||||
}
|
||||
|
||||
/** Parts of a URL */
|
||||
export type ParsedUrl = {
|
||||
protocol?: string,
|
||||
subdomain?: string,
|
||||
domain: string,
|
||||
host: string,
|
||||
port?: number,
|
||||
path?: string,
|
||||
query?: {[name: string]: string}
|
||||
fragment?: string
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns {RegExpExecArray}
|
||||
*/
|
||||
export function urlParser(url: string): ParsedUrl {
|
||||
const processed = new RegExp(
|
||||
'(?:(?<protocol>[\\w\\d]+)\\:\\/\\/)?(?:(?<user>.+)\\@)?(?<host>(?<domain>[^:\\/\\?#@\\n]+)(?:\\:(?<port>\\d*))?)(?<path>\\/.*?)?(?:\\?(?<query>.*?))?(?:#(?<fragment>.*?))?$',
|
||||
'gm').exec(url);
|
||||
const groups: ParsedUrl = <any>processed?.groups ?? {};
|
||||
const domains = groups.domain.split('.');
|
||||
if(groups['port'] != null) groups.port = Number(groups.port);
|
||||
if(domains.length > 2) {
|
||||
groups.domain = domains.splice(-2, 2).join('.');
|
||||
groups.subdomain = domains.join('.');
|
||||
}
|
||||
if(groups.query) {
|
||||
const split = (<any>groups.query).split('&'), query: any = {};
|
||||
split.forEach((q: any) => {
|
||||
const [key, val] = q.split('=');
|
||||
query[key] = val;
|
||||
});
|
||||
groups.query = query;
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Removes any null values from an object in-place
|
||||
* Removes any null values from an object in-place
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
@ -27,12 +27,12 @@ export function clean<T>(obj: T, undefinedOnly = false): Partial<T> {
|
||||
* Create a deep copy of an object (vs. a shallow copy of references)
|
||||
*
|
||||
* Should be replaced by `structuredClone` once released.
|
||||
*
|
||||
* @param {T} value Object to copy
|
||||
* @returns {T} Type
|
||||
* @deprecated Please use `structuredClone`
|
||||
*/
|
||||
export function deepCopy<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,8 +91,28 @@ export function dotNotation<T>(obj: any, prop: string, set?: T): T | undefined {
|
||||
}, obj);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively flatten a nested object, while maintaining key structure.
|
||||
* Convert object into URL encoded query string
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const query = encodeQuery({page: 1, size: 20});
|
||||
* console.log(query); // Output: "page=1&size=20"
|
||||
* ```
|
||||
*
|
||||
* @param {any} data - data to convert
|
||||
* @returns {string} - Encoded form data
|
||||
*/
|
||||
export function encodeQuery(data: any): string {
|
||||
return Object.entries(data).map(([key, value]) =>
|
||||
encodeURIComponent(key) + '=' + encodeURIComponent(<any>value)
|
||||
).join('&');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursively flatten a nested object, while maintaining key structure
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
@ -121,6 +141,7 @@ export function flattenObj(obj: any, parent?: any, result: any = {}) {
|
||||
|
||||
/**
|
||||
* Convert object to FormData
|
||||
*
|
||||
* @param target - Object to convert
|
||||
* @return {FormData} - Form object
|
||||
*/
|
||||
@ -173,6 +194,12 @@ export function isEqual(a: any, b: any): boolean {
|
||||
return Object.keys(a).every(key => isEqual(a[key], b[key]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Experimental: Combine multiple object prototypes into one
|
||||
*
|
||||
* @param target Object that will have prototypes added
|
||||
* @param {any[]} constructors Additionally prototypes that should be merged into target
|
||||
*/
|
||||
export function mixin(target: any, constructors: any[]) {
|
||||
constructors.forEach(c => {
|
||||
Object.getOwnPropertyNames(c.prototype).forEach((name) => {
|
||||
@ -186,30 +213,31 @@ export function mixin(target: any, constructors: any[]) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON but return the original string if it fails
|
||||
*
|
||||
* @param {string} json JSON string to parse
|
||||
* @return {string | T} Object if successful, original string otherwise
|
||||
*/
|
||||
export function JSONAttemptParse<T>(json: string): T | string {
|
||||
try { return JSON.parse(json); }
|
||||
catch { return json; }
|
||||
}
|
||||
|
||||
export function JSONSanitized(obj: any, space?: number) {
|
||||
/**
|
||||
* Convert an object to a JSON string avoiding any circular references.
|
||||
*
|
||||
* @param obj Object to convert to JSON
|
||||
* @param {number} space Format the JSON with spaces
|
||||
* @return {string} JSON string
|
||||
*/
|
||||
export function JSONSanitize(obj: any, space?: number): string {
|
||||
let cache: any[] = [];
|
||||
return JSON.parse(JSON.stringify(obj, (key, value) => {
|
||||
return JSON.stringify(obj, (key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (cache.includes(value)) return;
|
||||
cache.push(value);
|
||||
}
|
||||
return value;
|
||||
}, space));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert object into URL encoded string
|
||||
*
|
||||
* @param {any} data - data to convert
|
||||
* @returns {string} - Encoded form data
|
||||
*/
|
||||
export function urlEncode(data: any): string {
|
||||
return Object.entries(data).map(([key, value]) =>
|
||||
encodeURIComponent(key) + '=' + encodeURIComponent(<any>value)
|
||||
).join('&');
|
||||
}, space);
|
||||
}
|
||||
|
@ -1,5 +1,25 @@
|
||||
export type ProgressCallback = (progress: number) => any;
|
||||
|
||||
/**
|
||||
* A promise that fires the `onProgress` callback on incremental progress
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const promise = new Promise((resolve, reject, progress) => {
|
||||
* const max = 10;
|
||||
* for(let i = 0; i < max; i++) progress(i / max);
|
||||
* resolve(1);
|
||||
* });
|
||||
*
|
||||
* console.log(promise.progress);
|
||||
*
|
||||
* promise.onProgress(console.log)
|
||||
* .then(console.log)
|
||||
* .catch(console.error)
|
||||
* .finally(...);
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
export class PromiseProgress<T> extends Promise<T> {
|
||||
private listeners: ProgressCallback[] = [];
|
||||
|
||||
|
119
src/string.ts
119
src/string.ts
@ -1,28 +1,10 @@
|
||||
export function countChars(text: string, pattern: RegExp) {
|
||||
return text.length - text.replaceAll(pattern, '').length;
|
||||
}
|
||||
|
||||
export function createHex(length: number) {
|
||||
return Array(length).fill(null).map(() => Math.round(Math.random() * 0xF).toString(16)).join('');
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* String of all letters
|
||||
*
|
||||
*/
|
||||
const LETTER_LIST = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
/**
|
||||
* String of all numbers
|
||||
*
|
||||
*/
|
||||
const NUMBER_LIST = '0123456789';
|
||||
|
||||
@ -36,6 +18,37 @@ const SYMBOL_LIST = '~`!@#$%^&*()_-+={[}]|\\:;"\'<,>.?/';
|
||||
*/
|
||||
const CHAR_LIST = LETTER_LIST + NUMBER_LIST + SYMBOL_LIST;
|
||||
|
||||
/**
|
||||
* Generate a random hexadecimal value
|
||||
*
|
||||
* @param {number} length Number of hexadecimal place values
|
||||
* @return {string} Hexadecimal number as a string
|
||||
*/
|
||||
export function randomHex(length: number) {
|
||||
return Array(length).fill(null).map(() => Math.round(Math.random() * 0xF).toString(16)).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert number of bytes into a human-readable size
|
||||
*
|
||||
* @param {number} bytes Number of bytes
|
||||
* @param {number} decimals Decimal places to preserve
|
||||
* @return {string} Formated size
|
||||
*/
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract numbers from a string & create a formated phone number: +1 (123) 456-7890
|
||||
*
|
||||
* @param {string} number String that will be parsed for numbers
|
||||
* @return {string} Formated phone number
|
||||
*/
|
||||
export function formatPhoneNumber(number: string) {
|
||||
const parts = /(\+?1)?.*?(\d{3}).*?(\d{3}).*?(\d{4})/g.exec(number);
|
||||
if(!parts) throw new Error(`Number cannot be parsed: ${number}`);
|
||||
@ -46,7 +59,7 @@ export function formatPhoneNumber(number: string) {
|
||||
* Insert a string into another string at a given position
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* ```js
|
||||
* console.log(insertAt('Hello world!', ' glorious', 5);
|
||||
* // Output: Hello glorious world!
|
||||
* ```
|
||||
@ -60,12 +73,26 @@ export function insertAt(target: string, str: string, index: number): String {
|
||||
return `${target.slice(0, index)}${str}${target.slice(index + 1)}`;
|
||||
}
|
||||
|
||||
export function pad(text: any, length: number, char: string, start = true) {
|
||||
const t = text.toString();
|
||||
const l = length - t.length;
|
||||
if(l <= 0) return t;
|
||||
const padding = Array(~~(l / char.length)).fill(char).join('');
|
||||
return start ? padding + t : t + padding;
|
||||
/**
|
||||
* Add padding to string
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* const now = new Date();
|
||||
* const padded = now.getHours() + ':' + pad(now.getMinutes(), 2, '0');
|
||||
* console.log(padded); // Output: "2:05"
|
||||
* ```
|
||||
*
|
||||
* @param text Text that will be padded
|
||||
* @param {number} length Target length
|
||||
* @param {string} char Character to use as padding, defaults to space
|
||||
* @param {boolean} start Will pad start of text if true, or the end if false
|
||||
* @return {string} Padded string
|
||||
* @deprecated Please use `String.padStart` & `String.padEnd`
|
||||
*/
|
||||
export function pad(text: any, length: number, char: string = ' ', start = true) {
|
||||
if(start) return text.toString().padStart(length, char);
|
||||
return text.toString().padEnd(length, char);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -149,8 +176,50 @@ export function matchAll(value: string, regex: RegExp | string): RegExpExecArray
|
||||
return ret;
|
||||
}
|
||||
|
||||
/** Parts of a URL */
|
||||
export type ParsedUrl = {
|
||||
protocol?: string,
|
||||
subdomain?: string,
|
||||
domain: string,
|
||||
host: string,
|
||||
port?: number,
|
||||
path?: string,
|
||||
query?: {[name: string]: string}
|
||||
fragment?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Break a URL string into its parts for easy parsing
|
||||
*
|
||||
* @param {string} url URL string that will be parsed
|
||||
* @returns {RegExpExecArray} Parts of URL
|
||||
*/
|
||||
export function parseUrl(url: string): ParsedUrl {
|
||||
const processed = new RegExp(
|
||||
'(?:(?<protocol>[\\w\\d]+)\\:\\/\\/)?(?:(?<user>.+)\\@)?(?<host>(?<domain>[^:\\/\\?#@\\n]+)(?:\\:(?<port>\\d*))?)(?<path>\\/.*?)?(?:\\?(?<query>.*?))?(?:#(?<fragment>.*?))?$',
|
||||
'gm').exec(url);
|
||||
const groups: ParsedUrl = <any>processed?.groups ?? {};
|
||||
const domains = groups.domain.split('.');
|
||||
if(groups['port'] != null) groups.port = Number(groups.port);
|
||||
if(domains.length > 2) {
|
||||
groups.domain = domains.splice(-2, 2).join('.');
|
||||
groups.subdomain = domains.join('.');
|
||||
}
|
||||
if(groups.query) {
|
||||
const split = (<any>groups.query).split('&'), query: any = {};
|
||||
split.forEach((q: any) => {
|
||||
const [key, val] = q.split('=');
|
||||
query[key] = val;
|
||||
});
|
||||
groups.query = query;
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create MD5 hash using native javascript
|
||||
*
|
||||
* @param d String to hash
|
||||
* @returns {string} Hashed string
|
||||
*/
|
||||
|
26
src/time.ts
26
src/time.ts
@ -1,13 +1,17 @@
|
||||
export function formatDate(date: Date | number | string) {
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat("en-us", {
|
||||
weekday: "long",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true
|
||||
}).format(d);
|
||||
/**
|
||||
* Return date formated highest to lowest: YYYY-MM-DD H:mm AM
|
||||
*
|
||||
* @param {Date | number | string} date Date or timestamp to convert to string
|
||||
* @return {string} Formated date
|
||||
*/
|
||||
export function formatDate(date: Date | number | string): string {
|
||||
if(typeof date == 'number' || typeof date == 'string') date = new Date(date);
|
||||
let hours = date.getHours(), postfix = 'AM';
|
||||
if(hours >= 12) {
|
||||
if(hours > 12) hours -= 12;
|
||||
postfix = 'PM';
|
||||
} else if(hours == 0) hours = 12;
|
||||
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}, ${hours}:${date.getMinutes().toString().padStart(2, '0')} ${postfix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -17,6 +21,7 @@ export function formatDate(date: Date | number | string) {
|
||||
* ```js
|
||||
* await sleep(1000) // Pause for 1 second
|
||||
* ```
|
||||
*
|
||||
* @param {number} ms - Time to pause for in milliseconds
|
||||
* @returns {Promise<unknown>} - Resolves promise when it's time to resume
|
||||
*/
|
||||
@ -33,6 +38,7 @@ export function sleep(ms: number): Promise<void> {
|
||||
* setTimeout(() => wait = false, 1000);
|
||||
* await sleepUntil(() => loading); // Won't continue until loading flag is false
|
||||
* ```
|
||||
*
|
||||
* @param {() => boolean} fn Return true to continue
|
||||
* @param {number} checkInterval Run function ever x milliseconds
|
||||
* @return {Promise<void>} Callback when sleep is over
|
||||
|
20
src/types.ts
Normal file
20
src/types.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Return keys on a type as an array of strings
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* type Person = {
|
||||
* firstName: string;
|
||||
* lastName: string;
|
||||
* age: number;
|
||||
* }
|
||||
*
|
||||
* const keys = typeKeys<Person>();
|
||||
* console.log(keys); // Output: ["firstName", "lastName", "age"]
|
||||
* ```
|
||||
*
|
||||
* @return {Array<keyof T>} Available keys
|
||||
*/
|
||||
export function tyoeKeys<T extends object>() {
|
||||
return Object.keys(<T>{}) as Array<keyof T>;
|
||||
}
|
Reference in New Issue
Block a user