Skip to content

Instantly share code, notes, and snippets.

@jomifepe
Last active September 28, 2023 23:51
Show Gist options
  • Save jomifepe/566a896d77b590e5f360fadc48218500 to your computer and use it in GitHub Desktop.
Save jomifepe/566a896d77b590e5f360fadc48218500 to your computer and use it in GitHub Desktop.
TypeScript, React & Node Snippets

TypeScript, React & Node snippets.

TypeScript Logo React Logo Node Logo

// 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>;
// 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 };
};
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
);
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');
/**
* 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)];
// 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 {};
/**
* 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;
};
/**
* 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;
};
};
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];
}
/**
* 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]>;
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) })),
});
// 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>
);
/**
* 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]>;
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