Skip to content

Instantly share code, notes, and snippets.

@timkinnane
Last active February 14, 2023 16:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save timkinnane/b1baab92d6432fa9bdb570328a73cbb1 to your computer and use it in GitHub Desktop.
Save timkinnane/b1baab92d6432fa9bdb570328a73cbb1 to your computer and use it in GitHub Desktop.
Typescript Generic Utils for Functional Programming
/**
* Make all optional except given keys.
* @example
* type ABC = { a: any, b: any, c: any }
* type OnlyAB = Only<ABC, 'a' | 'b'>
* // ☝️ OnlyAB = { a, b, c? }
*/
export type Only<T, K extends keyof T> = Partial<T> & Required<Pick<T, K>>
/**
* Makes given keys optional.
* @example
* type ABC = { a: any, b: any, c: any }
* type OptionalAB = Optional<ABC, 'a' | 'b'>
* // ☝️ OptionalAB = { a?, b?, c }
*/
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
/**
* Make given keys required.
* @example
* type ABC = { a?: any, b?: any, c?: any }
* type IncludeAB = Include<ABC, 'a' | 'b'>
* // ☝️ IncludeAB = { a, b, c? }
*/
export type Include<T, K extends keyof T> = T & Required<Pick<T, K>>
/**
* Get nested type at key.
* @example
* type ABC = { a: string, b: boolean, c: number }
* type UnpackC = Unpack<ABC, 'c'>
* // ☝️ UnpackC = number
*/
export type Unpack<T, K> = K extends keyof T ? T[K] : never;
/**
* Make a tuple from unpacked types on an interface.
* @example
* type ABC = { a: string, b: boolean, c: number }
* type UnpackTupleAB = UnpackTuple<ABC, ['a', 'b']>
* // ☝️ UnpackTupleAB = [string, boolean]
*/
export type UnpackTuple<T, K extends Array<keyof T>> = {
[I in keyof K]: Unpack<T, K[I]>
}
/**
* Function type uses types from interface as args and optional return type.
* @example
* type ABC = { a: string, b: boolean, c: number }
* type UnpackFunctionCfromAB = UnpackFunction<ABC, ['a', 'b'], 'c'>
* // ☝️ UnpackFunctionCfromAB = (args_0: string, args_1: boolean) => number
* type UnpackFunctionA = UnpackFunction<ABC, ['a']>
* // ☝️ UnpackFunctionA = (args_0: string) => any
*/
export type UnpackFunction<
T,
Params extends Array<keyof T> = [],
Returns extends keyof T = any
> = (...args: UnpackTuple<T, Params>) => Unpack<T, Returns>
/*
These generics can be applied to create types for pipelines that operate on an
interface, where some attributes are given as input but others are assigned by
pipeline functions, but can be trusted to exist on the output type.
BELOW IS FOR EXAMPLE ONLY...
*/
/** Pipeline functions return the type they're given after operating on props. */
interface PipelineFunction<T> {
(input: T): T
}
/** Generic pipe function accepts input and output types. */
function propsPipe<I extends {}, O extends I> (...fns: PipelineFunction<I>[]) {
return (input: I) => fns.reduce((res, fn) => fn(res), input) as O
}
/** Example pipeline props. */
type AllProps = { value: number, label: string }
/** Props available on pipeline input. */
type InputProps = Only<AllProps, 'value'>
/** Props available on output, assigned by pipeline functions. */
type OutputProps = Include<InputProps, 'label'>
/**
* Functions in pipeline know which props they can depend on.
* Compiler will warn if they aren't passing everything along.
*/
const assignLabel: PipelineFunction<InputProps> = (props) => {
props.label = `The value is ${props.value.toString()}`
return props
}
/** Entry point for the pipeline takes the only required prop as an argument. */
const propsFromValue = (value: Unpack<InputProps, 'value'>) =>
propsPipe<InputProps, OutputProps>(
assignLabel
// [, otherFuncs]
)({ value })
// FINALLY...
// Input and output are typed. Note we only define the prop types once.
console.log(propsFromValue(100).label)
// ☝️ "The value is 100"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment