Last active
June 8, 2024 00:29
-
-
Save ih2502mk/88901249cef02335d6081589330b1fe1 to your computer and use it in GitHub Desktop.
This file contains 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
interface LineItem { | |
id: string, | |
amount: number, | |
name: string, | |
} | |
type TransformFn<T1, T2> = (item: T1) => T2 | |
// v0 This function takes a set of transform functions and call them in sequence | |
// someone could say it "composes" them | |
function createTransform< | |
T1, T2, T3, T4 | |
>(ts: [ | |
TransformFn<T1, T2>, | |
TransformFn<T2, T3>, | |
TransformFn<T3, T4> | |
]): (item: T1) => T4 | |
// There is a limitation: | |
// if number of transforms passed to this one as arguments exceeds 3 we'll have an error | |
// to bypass this limitation we can define this function for some large number of | |
// arguments and hope that noone needs to compose more than 23 transfroms | |
// OR | |
// since the result of createTransform is itself a transform we can tell users that | |
// if the number does exceed they should split it in half and | |
// call createTransform on halfs like | |
// createTransform(createTransform([a,b,c]), createTransform([d,e,f])) | |
//==== | |
// v1 We can define the function with arbitrary number of parameters but it will have to use any | |
function createTransform< | |
T1, T2, T3, T4 | |
>(ts: [ | |
TransformFn<T1, T2>, | |
TransformFn<T2, T3>, | |
TransformFn<T3, T4>, | |
...rest:TransformFn<any, any>[] | |
]): (item: T1) => any | |
// Limitation of this approach is that | |
// if number of transforms exceeds number of properly defined arguments | |
// we will end up with any | |
// This is an implementation of createTransform | |
// valid syntax (repeated funciton defs) due to TS overloading | |
function createTransform(ts: TransformFn<any, any>[]): (item: any) => any { | |
return (item) => ts.reduce((transformedItem, tf) => { | |
return tf(transformedItem) | |
}, item) | |
} | |
// Example transforms | |
function a(item: LineItem) { | |
return {...item, addedThing_a: 'aaa'} | |
} | |
function b(item: ReturnType<typeof a>) { | |
return {...item, addedThing_b: 'bbb'} | |
} | |
function c(item: ReturnType<typeof b>) { | |
return {...item, addedThing_c: 'ccc'} | |
} | |
function d(item: ReturnType<typeof c>) { | |
return {...item, addedThing_d: 'ddd'} | |
} | |
// 3 args -> everything is typed neatly | |
const tr3 = createTransform([a, b, c]); | |
// 4 args -> return type ends up being any :( | |
const tr4 = createTransform([a, b, c, d]); | |
const item_abc = tr4({id: '123-qwe', amount: 100, name: 'foo'}); | |
// CAN WE DO BETTER? | |
// Some thoughts with made up syntax | |
//// function betterCreateTransform<TypeList>(ts: MakeTransformFn<Pairwise<TypeList>>): (a: TypeList_0): TypeList_Last | |
// This is not exactly useful | |
// In our case (i.e. exposing some transform api to developers/consumers of our lib) | |
// it might be better to derive types the other way around — from transform functions provided by users | |
//// function evenBetterCreateTransform<TypeList = List<TypesFromArgs>>(ts: List<TransformFn>): (a: TypeList_First): TypeList_Last | |
// Given list (tuple) of functions we collect types of their argumets and returt type of the last one | |
// and make return type of transform composition to be a function from first item in a list to last item in a list | |
// define a generic function that covers all possible functions alternatively could use buit in Function | |
type GenericFunction = (...args: any) => any; | |
// jsut check that it works on one of our transforms | |
type A = typeof a extends GenericFunction ? typeof a : never; | |
// define an interface that works on our transform funcitons | |
type TransformList<Fns extends GenericFunction[]> = { | |
// this technically could be even defined as Fns[0], but | |
// it would break if we would try to call it against empty array of in place of Fns | |
// thus | |
first: Fns extends [infer First, ...infer _] ? First : never, | |
last: Fns extends [...infer _, infer Last] ? Last : never, | |
} | |
type AList = TransformList<[typeof a, typeof b, typeof c]> | |
type FirstestArg = Parameters<AList['first']>[0] | |
type LastestArg = ReturnType<AList['last']> | |
function evenBetterCreateTransform<Fns extends GenericFunction[]> | |
(fns: Fns): (input: Parameters<TransformList<Fns>['first']>[0]) => ReturnType<TransformList<Fns>['last']> { | |
return (input) => { | |
return fns.reduce<ReturnType<TransformList<Fns>['last']>>((trInput, fn) => { | |
return fn(trInput) | |
}, input) | |
} | |
} | |
// let's test it out | |
const evenBetterTransform_ = evenBetterCreateTransform([a,b,c,d]) | |
// typeof evenBetterTransform is (input: never) => never — huh? not what we expected | |
// The reason is that as far as TransformList type [a,b,c,d] does not extend a tuple | |
// it's some arbitrary array of functions | |
const evenBetterTransform = evenBetterCreateTransform([a,b,c,d] as const); | |
// now types are correct | |
// Improve the ergonomics i.e. make it look slightly nicer | |
const _IN: unique symbol = Symbol('IN') | |
type IN = typeof _IN; | |
const _OUT: unique symbol = Symbol('OUT') | |
type OUT = typeof _OUT; | |
type ExtractEnds<Fns extends GenericFunction[]> = { | |
[_IN]: Fns[0] extends GenericFunction ? Parameters<Fns[0]>[0] : never, | |
[_OUT]: Fns extends [...infer _, infer Last] ? Last extends GenericFunction ? ReturnType<Last> : never : never, | |
} | |
type BList = ExtractEnds<[typeof a, typeof b, typeof c]> | |
function bestTransformSoFar<Fns extends GenericFunction[]>(fns: Fns): (input: ExtractEnds<Fns>[IN]) => ExtractEnds<Fns>[OUT] { | |
return (input: ExtractEnds<Fns>[IN]) => { | |
return fns.reduce((trInput, fn) => { | |
return fn(trInput) | |
}, input) | |
} | |
} | |
const bestTransform = bestTransformSoFar([a,b,c,d] as const) | |
const aaa = bestTransform({id: '123', amount: 123, name: 'foo'}) | |
console.log(aaa) | |
// so that `as const` kinda bother me ... | |
// CAN WE DO EVEN BETTER ??? | |
// What if instead of passing and array we just define bestTransformSoFar as a variadic function? | |
function totesBestTransform<Fns extends GenericFunction[]>(...fns: Fns): (input: ExtractEnds<Fns>[IN]) => ExtractEnds<Fns>[OUT] { | |
return (input: ExtractEnds<Fns>[IN]) => { | |
return fns.reduce((trInput, fn) => { | |
return fn(trInput) | |
}, input) | |
} | |
} | |
const totesTransform = totesBestTransform(a,b,c,d) | |
const bbb = totesTransform({id: '123', amount: 123, name: 'foo'}) | |
console.log(bbb) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Copy paste this into ts playground to see whats going on...
Inspired by