TypeScript, React & Node snippets.
Last active
September 28, 2023 23:51
-
-
Save jomifepe/566a896d77b590e5f360fadc48218500 to your computer and use it in GitHub Desktop.
TypeScript, React & Node Snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// helper types and methods for returning data and errors and narrowing their types | |
// inspired by Rust Result: https://doc.rust-lang.org/rust-by-example/error/result.html | |
export type None = null | undefined; | |
export type Err<Error> = { | |
data: None; | |
err: Error; | |
isErr: true; | |
isOk: false; | |
unwrapOr: <T extends unknown>(fallback: T) => T; | |
}; | |
export type Ok<Data> = { | |
data: Data; | |
err: None; | |
isErr: false; | |
isOk: true; | |
unwrapOr: <T extends unknown>(fallback: T) => Data; | |
}; | |
/** | |
* @example | |
* const getApiData = async (): Promise<Result<ApiData, ApiError>> => { | |
* try { | |
* // data getting code | |
* return Ok({ message: 'hello' }); | |
* } catch (error) { | |
* return Err({ errorCode: 500 }); | |
* } | |
* }; | |
* const result = await getApiData(); | |
* const data = result.unwrapOr(null); // data = ApiData | null | |
* | |
* if (result.isOk) { | |
* console.log(result.data.message); // 'hello' | |
* } else { | |
* console.log(result.err.errorCode); // 500 | |
* } | |
*/ | |
export const Err = <Error = null>(err: Error): Err<Error> => ({ | |
data: null, | |
err, | |
isErr: true, | |
isOk: false, | |
unwrapOr: <T>(value: T) => value, | |
}); | |
/** | |
* @example | |
* const getApiData = async (): Promise<Result<ApiData, ApiError>> => { | |
* try { | |
* // data getting code | |
* return Ok({ message: 'hello' }); | |
* } catch (error) { | |
* return Err({ errorCode: 500 }); | |
* } | |
* }; | |
* const result = await getApiData(); | |
* const data = result.unwrapOr(null); // data = ApiData | null | |
* | |
* if (result.isOk) { | |
* console.log(result.data.message); // 'hello' | |
* } else { | |
* console.log(result.err.errorCode); // 500 | |
* } | |
*/ | |
export const Ok = <Data>(data: Data): Ok<Data> => ({ | |
data, | |
err: null, | |
isErr: false, | |
isOk: true, | |
unwrapOr: () => data, | |
}); | |
export type Result<Data, Error> = Ok<Data> | Err<Error>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Uses Node APIs. Can be used on a raycast extension | |
type EncryptedContent = { iv: string; content: string }; | |
const get32BitSecretKeyBuffer = (key: string) => | |
Buffer.from(createHash("sha256").update(key).digest("base64").slice(0, 32)); | |
export const useContentEncryptor = (secretKey: string) => { | |
const cipherKeyBuffer = useRef<Buffer>(get32BitSecretKeyBuffer(secretKey.trim())); | |
useEffect(() => { | |
cipherKeyBuffer.current = get32BitSecretKeyBuffer(secretKey.trim()); | |
}, [secretKey]); | |
function encrypt(data: string): EncryptedContent { | |
const ivBuffer = randomBytes(16); | |
const cipher = createCipheriv("aes-256-cbc", cipherKeyBuffer.current, ivBuffer); | |
const encryptedContentBuffer = Buffer.concat([cipher.update(data), cipher.final()]); | |
return { iv: ivBuffer.toString("hex"), content: encryptedContentBuffer.toString("hex") }; | |
} | |
function decrypt(data: EncryptedContent) { | |
const decipher = createDecipheriv("aes-256-cbc", cipherKeyBuffer.current, Buffer.from(data.iv, "hex")); | |
const decryptedContentBuffer = Buffer.concat([decipher.update(Buffer.from(data.content, "hex")), decipher.final()]); | |
return decryptedContentBuffer.toString(); | |
} | |
return { encrypt, decrypt }; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Data = { first: string; second: string }; | |
const initialData: Data = { first: "firstValue", second: "secondValue" }; | |
const [data, setData] = useReducer( | |
(current: Data, update: Partial<Data>) => ({ | |
...current, | |
...update, | |
}), | |
initialData | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const capitalize = (value: string) => value.charAt(0).toUpperCase() + value.slice(1); | |
/** Quickly create a component with shared classes to use in multiple places */ | |
const styled = <C extends keyof JSX.IntrinsicElements>( | |
component: C, | |
commonClassName: string, | |
displayName = `Styled${capitalize(component)}`, | |
) => { | |
const StyledComponent = ({ className, ...remainingProps }: JSX.IntrinsicElements[C]) => | |
createElement(component, { | |
...remainingProps, | |
className: classNames(commonClassName, className), | |
}); | |
StyledComponent.displayName = displayName; | |
return StyledComponent; | |
}; | |
const Grid = styled('div', 'grid grid-cols-3 gap-4'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Recursively iterates through an object and returns it's keys as a union, separated by | |
* dots, depending on the depth of the object. | |
* | |
* Does not iterate through arrays to prevent extracting it's methods (e.g. push and pop) | |
* | |
* @example | |
* | |
* type Person = { | |
* name: string; | |
* dog: { age: number } | |
* } | |
* type PersonKeys = RecursiveKeyOf<Person> | |
* // PersonKeys: 'name' | 'dog' | 'dog.age' | |
*/ | |
export type RecursiveKeyOf<TObj extends object> = { | |
[TKey in keyof TObj & string]: RecursiveKeyOfHandleValue<TObj[TKey], `${TKey}`>; | |
}[keyof TObj & string]; | |
type RecursiveKeyOfHandleValue<TValue, Text extends string> = TValue extends object | |
? TValue extends { pop: any; push: any } | |
? Text | |
: Text | `${Text}${RecursiveKeyOfInner<TValue>}` | |
: Text; | |
type RecursiveKeyOfInner<TObj extends object> = { | |
[TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue<TObj[TKey], `.${TKey}`>; | |
}[keyof TObj & (string | number)]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// types/global.d.ts | |
declare global { | |
interface ObjectConstructor { | |
keys<T extends object>(obj: T): (keyof T)[]; | |
/** `Object.entries` that preserves the type of the object keys */ | |
typedEntries<T extends object>(obj: T): { [K in keyof T]: [K, T[K]] }[keyof T][]; | |
} | |
} | |
export {}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Omits properties from an object, including nested one. | |
* | |
* Known limitations: | |
* - Doesn't work with arrays | |
* - Doesn't remove the parent property if it's empty | |
* - The return type is still the original object, as omitting the keys using types, and still having | |
* suggestions on the paths, is impossible. Suggestion: Cast it to the correct type or any before using it. | |
* | |
* @example | |
* const obj = { | |
* name: 'John', | |
* address: { | |
* streetName: 'Test Street', | |
* postalCode: '2222-222', | |
* } | |
* } | |
* | |
* omitProps(obj, 'address.postalCode') // result: { name: 'John', address: { streetName: 'Test Street' }} | |
*/ | |
const omitProps = <T extends Record<string, any>>(obj: T, paths: RecursiveKeyOf<T> | RecursiveKeyOf<T>[]) => { | |
const result = { ...obj }; | |
for (const path of Array.isArray(paths) ? paths : [paths]) { | |
const indexOfDot = path.indexOf('.'); | |
if (indexOfDot !== -1) { | |
const firstProp = path.slice(0, indexOfDot); | |
const remainingProps = path.slice(indexOfDot + 1, path.length); | |
obj[firstProp as keyof T] = omitKeys(obj[firstProp], remainingProps); | |
} else if (Object.prototype.hasOwnProperty.call(obj, path)) { | |
delete result[path]; | |
} | |
} | |
return result; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked. | |
* | |
* @example | |
* const handleChange = () => // API call | |
* const handleInputChange = debounce(handleChange, 300); | |
* | |
* <input onChange={handleInputChange} /> | |
*/ | |
export const debounce = <R, A extends any[]>(fn: (...args: A) => R, delay = 200) => { | |
let timeout: NodeJS.Timeout; | |
return (...args: any) => { | |
clearTimeout(timeout as NodeJS.Timeout); | |
timeout = setTimeout(() => fn(...args), delay); | |
}; | |
}; | |
/** | |
* Creates a throttled function that only invokes func at most once per every wait milliseconds. | |
* | |
* @example | |
* const handleChange = () => // API call | |
* const handleInputChange = throttle(handleChange, 300); | |
* | |
* <input onChange={handleInputChange} /> | |
*/ | |
export const throttle = <R, A extends any[]>(fn: (...args: A) => R, delay = 200) => { | |
let wait = false; | |
return (...args: A) => { | |
if (wait) return undefined; | |
const val = fn(...args); | |
wait = true; | |
window.setTimeout(() => { | |
wait = false; | |
}, delay); | |
return val; | |
}; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type UseLazyQueryOptions<TData = unknown, TError = unknown> = Omit< | |
UseQueryOptions<TData, TError, TData, QueryKey>, | |
'queryKey' | 'queryFn' | |
> & { | |
resetOnSuccess?: boolean; | |
resetOnError?: boolean; | |
}; | |
export type LazyQueryResult<TData = unknown, TError = unknown> = { | |
data: TData | undefined; | |
error: TError | null; | |
}; | |
export type UseLazyQueryResult<TData = unknown, TError = unknown, TVariables = unknown> = [ | |
triggerFn: (variables?: TVariables) => Promise<LazyQueryResult<TData, TError>>, | |
queryResult: UseQueryResult<TData, TError>, | |
]; | |
export type LazyQueryFunctionArgs<TQueryKey extends QueryKey = QueryKey, TVariables = unknown> = { | |
context: QueryFunctionContext<TQueryKey>; | |
variables: TVariables; | |
}; | |
export type LazyQueryFunction<TData = unknown, TVariables = unknown> = ( | |
args: LazyQueryFunctionArgs<QueryKey, TVariables>, | |
) => TData | Promise<TData>; | |
type TriggerFnPromiseHolder<TData, TError> = { | |
resolve?: (value: LazyQueryResult<TData, TError>) => void; | |
}; | |
const TRIGGER_FN_PROMISE_REF_INITIAL_VALUE = { resolve: undefined }; | |
/** | |
* `useQuery` but lazy. Returns a `trigger` function that can be called to execute the query. | |
* | |
* Why not use a `useMutation`? https://github.com/TanStack/query/discussions/1205#discussioncomment-2886537 | |
* | |
* @example | |
* // Simple usage | |
* | |
* const [fetchUsers, { data, error }] = useLazyQuery<User[]>('fetch-users', () => fetch('/users')); | |
* | |
* useEffect(() => { | |
* fetchData(); | |
* }, []) | |
* | |
* @example | |
* // Awaiting and getting the query result from the trigger function | |
* | |
* const [users, setUsers] = useState<User[]>(); | |
* const [error, setError] = useState<ApiError>(); | |
* const [fetchUsers] = useLazyQuery<User[], ApiError>('fetch-users', () => fetch('/users')); | |
* | |
* const onClick = async () => { | |
* const { data, error } = await fetchUsers(); | |
* setUsers(data); | |
* setError(error); | |
* } | |
* | |
* @example | |
* // Passing parameters to the fetching function using the trigger function | |
* | |
* const [fetchUserById, { data, error }] = useLazyQuery<User, {}, { id: string }>( | |
* 'fetch-user-by-id', | |
* ({ variables }) => fetch(`/users/${variables.id}}`), | |
* ); | |
* | |
* const handleGetUserClick = () => { | |
* fetchUserById(3); | |
* } | |
*/ | |
function useLazyQuery<TData = unknown, TError = unknown, TVariables = unknown>( | |
queryKey: Parameters<typeof useQuery>[0], | |
queryFn: LazyQueryFunction<TData, TVariables>, | |
options?: UseLazyQueryOptions<TData, TError>, | |
): UseLazyQueryResult<TData, TError, TVariables> { | |
const { resetOnSuccess = true, resetOnError = true, enabled, ...queryOptions } = options ?? {}; | |
const [shouldQuery, setShouldQuery] = useState(false); | |
const queryVariablesRef = useRef<TVariables>(); | |
const triggerFnPromiseRef = useRef<TriggerFnPromiseHolder<TData, TError>>( | |
TRIGGER_FN_PROMISE_REF_INITIAL_VALUE, | |
); | |
const queryResult = useQuery<TData, TError, TData, QueryKey>( | |
queryKey, | |
(context) => queryFn({ context, variables: queryVariablesRef.current as TVariables }), | |
{ | |
...(queryOptions || {}), | |
onSettled: (data, error) => { | |
triggerFnPromiseRef.current.resolve?.({ data, error }); | |
triggerFnPromiseRef.current = TRIGGER_FN_PROMISE_REF_INITIAL_VALUE; | |
queryVariablesRef.current = undefined; | |
queryOptions?.onSettled?.(data, error); | |
}, | |
onSuccess: (data) => { | |
if (resetOnSuccess) setShouldQuery(false); | |
queryOptions?.onSuccess?.(data); | |
}, | |
onError: (error) => { | |
if (resetOnError) setShouldQuery(false); | |
queryOptions?.onError?.(error); | |
}, | |
enabled: shouldQuery && enabled, | |
}, | |
); | |
const triggerFn = useCallback( | |
(variables?: TVariables) => | |
new Promise<LazyQueryResult<TData, TError>>((resolve) => { | |
// NOTE: currently, if a query is pending, new calls will be ignored | |
if (!shouldQuery && !triggerFnPromiseRef.current.resolve) { | |
triggerFnPromiseRef.current = { resolve }; | |
queryVariablesRef.current = variables; | |
setShouldQuery(true); | |
} | |
}), | |
[shouldQuery], | |
); | |
return [triggerFn, queryResult]; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Enforces a fixed size hardcoded array. | |
* @see https://github.com/microsoft/TypeScript/issues/18471#issuecomment-776636707 for the source of the solution. | |
* @see https://github.com/microsoft/TypeScript/issues/26223#issuecomment-674514787 another approach, that might be useful if the one above shows any limitation. | |
* @example | |
* const fixedArray: FixedSizeArray<string, 3> = ['a', 'b', 'c']; | |
*/ | |
export type FixedLengthArray<T, N extends number> = N extends N | |
? number extends N | |
? T[] | |
: FixedLengthArrayRecursive<T, N, []> | |
: never; | |
type FixedLengthArrayRecursive<T, N extends number, R extends unknown[]> = R['length'] extends N | |
? R | |
: FixedLengthArrayRecursive<T, N, [T, ...R]>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type BuildTestFactoryGetterProps<TModel extends unknown> = ( | |
index: number, | |
) => Partial<TModel>; | |
export type BuildTestFactoryArrayProps< | |
TModel extends unknown, | |
TCount extends number, | |
> = FixedLengthArray<Partial<TModel>, TCount>; | |
type BuildTestFactoryProps<TModel extends unknown, TCount extends number> = | |
| BuildTestFactoryGetterProps<TModel> | |
| BuildTestFactoryArrayProps<TModel, TCount>; | |
const isGetterProps = <TModel extends unknown>( | |
props: unknown, | |
): props is BuildTestFactoryGetterProps<TModel> => typeof props === 'function'; | |
const isArrayProps = <TModel extends unknown>( | |
props: unknown, | |
index: number, | |
): props is Array<TModel> => Array.isArray(props) && index < props.length; | |
const getOverrideProps = <TModel extends unknown, TCount extends number>( | |
props: BuildTestFactoryProps<TModel, TCount> | undefined, | |
index: number, | |
): Partial<TModel> => { | |
let overrideProps: Partial<TModel> = {}; | |
if (isGetterProps<TModel>(props)) { | |
overrideProps = props(index); | |
} else if (isArrayProps<TModel>(props, index)) { | |
overrideProps = props[index]; | |
} | |
return overrideProps; | |
}; | |
/** | |
* Provides functions to create items from a given factory. | |
* @example | |
* | |
* const UserFactory = buildTestFactory<{ name: string, age: number }>((index) => ({ | |
* name: faker.name.firstName(), | |
* age: faker.datatype.number(1, index === 0 ? 50 : 100), | |
* })); | |
* // single item | |
* UserFactory.create(); | |
* // single item with overriden props | |
* UserFactory.create({ name: 'John' }); | |
* // multiple items | |
* UserFactory.createMany(3); | |
* // multiple items with overriden props on every item | |
* UserFactory.createMany(3, () => ({ name: 'John' })); | |
* // multiple items with overriden props per index | |
* UserFactory.createMany(3, [{ name: 'John' }, { name: 'Jane' }, { name: 'Jack' }]); | |
*/ | |
export const buildTestFactory = <TModel extends ObjectOfAny>( | |
factory: (index?: number) => TModel, | |
) => ({ | |
create: (props?: Partial<TModel>): TModel => ({ ...factory(), ...props }), | |
createMany: <TCount extends number>( | |
count = 10 as TCount, | |
props?: BuildTestFactoryProps<TModel, TCount>, | |
): TModel[] => | |
range(1, count).map((_, index) => ({ ...factory(index), ...getOverrideProps(props, index) })), | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Playground: https://codesandbox.io/s/ure7qi | |
type TabsContextType = { | |
activeTab: string; | |
setActiveTab: (label: string) => void; | |
}; | |
const TabsContext = createContext<TabsContextType | undefined>(undefined); | |
export const useTabContext = () => { | |
const context = useContext(TabsContext); | |
if (!context) { | |
throw new Error("This component must be used within a <Tabs> component."); | |
} | |
return context; | |
}; | |
const Tabs = ({ children }: { children: ReactNode; }) => { | |
const [activeTab, setActiveTab] = useState("a"); | |
return ( | |
<TabsContext.Provider value={{ activeTab, setActiveTab }}> | |
{children} | |
</TabsContext.Provider> | |
); | |
}; | |
Tabs.Tab = Tab; | |
Tabs.Content = Content; | |
const Tab = ({ id, children }: { id: string; children: ReactNode; }) => { | |
const { setActiveTab } = useTabContext(); | |
const selectTab = () => setActiveTab(id); | |
return ( | |
<div className="tab"> | |
<button onClick={selectTab}>{children}</button> | |
</div> | |
); | |
}; | |
const Content = ({ id, children }: { id: string; children: React.ReactNode; }) => { | |
const { activeTab } = useTabContext(); | |
if (activeTab !== id) return null; | |
return <div>{children}</div>; | |
}; | |
const App = () => ( | |
<div className="App"> | |
<Tabs> | |
<Tabs.Tab id="a">Tab A</Tabs.Tab> | |
<Tabs.Tab id="b">Tab B</Tabs.Tab> | |
<Tabs.Content id="a"> | |
Tab A content ๐ | |
</Tabs.Content> | |
<Tabs.Content id="b"> | |
Tab B Content ๐ | |
</Tabs.Content> | |
</Tabs> | |
</div> | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Enforces a fixed size hardcoded array. | |
* @see https://github.com/microsoft/TypeScript/issues/18471#issuecomment-776636707 for the source of the solution. | |
* @see https://github.com/microsoft/TypeScript/issues/26223#issuecomment-674514787 another approach, that might be useful if the one above shows any limitation. | |
* @example | |
* const fixedArray: FixedSizeArray<string, 3> = ['a', 'b', 'c']; | |
*/ | |
export type FixedLengthArray<T, N extends number> = N extends N | |
? number extends N | |
? T[] | |
: FixedLengthArrayRecursive<T, N, []> | |
: never; | |
type FixedLengthArrayRecursive<T, N extends number, R extends unknown[]> = R['length'] extends N | |
? R | |
: FixedLengthArrayRecursive<T, N, [T, ...R]>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type ValidParams = Record<string, number | string | undefined | null>; | |
type Options<P extends ValidParams> = { | |
defaultValues?: P; | |
prependSeparator?: boolean; | |
}; | |
/** | |
* Converts an object of params to a query string | |
* | |
* @example | |
* | |
* buildQueryString({ page: 2, search: undefined }); | |
* // Result: page=2 | |
* buildQueryString({ page: 2, search: undefined }, { prependSeparator: true }); | |
* // Result: ?page=2 | |
* buildQueryString({ page: null, search: 'test' }, { defaultValues: { page: 0 } }); | |
* // Result: page=0&search=test | |
*/ | |
function buildQueryString<P extends ValidParams>(params: P, options?: Options<P>) { | |
if (!params) return ''; | |
const queryString = Object.entries(params) | |
.reduce((searchParams, [key, value]) => { | |
if (value == null) { | |
const defaultValue = options?.defaultValues?.[key]; | |
if (defaultValue == null) return searchParams; | |
searchParams.append(key, String(defaultValue)); | |
} else { | |
if (['function', 'object', 'symbol'].includes(typeof value)) return searchParams; | |
searchParams.append(key, String(value)); | |
} | |
return searchParams; | |
}, new URLSearchParams()) | |
.toString(); | |
if (!options?.prependSeparator) return queryString; | |
return `?${queryString}`; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment