I am trying to define types as collections of shaped types using generics but am either doing something wrong or TS cannot do it. I've tried a lot of things in the past week but most of it is "lost" due to trying other things over and over. I am not sure if its possible, but my guess is, it should be. I will try to simply this as much as possible, but it will be a longer post, sorry no TLDR for this one.
The amount of types needed to produce a minimal-viable-reproducible-example for this particular issue is like 200 lines of types-only code, most of which are irrelevant but because they all chain one into another, its hard to extract a simple example from them, thus I will explain the issue at hand and post a link to a typescript playground with the code in case someone needs to take a look.
For context, I am developing some form of Redux Extension, or Redux2.0 if you will.
I am trying to define a type for a "return value" of a function which takes in an "array" of Bundles and returns a result which is based on those bundles. What is a bundle you ask? Its sort of a "Redux Plugin", something like this:
interface Bundle< S = any, Args extends object = object, ActionExt extends object = object > { name: string reducer?: Reducer<S> selectors?: { [key: string]: Selector } reactors?: { [key: string]: Reactor } actions?: { [key: string]: AnyAction | ThunkAction | ActionExt | ?PossibleFutureProblem? } priority?: number init?: (store: Store) => void args?: ArgGenerator<Args> middleware?: MiddlewareGenerator<ActionExt> persist?: string[] } So once the function processes multiples of these bundles, it is suppose to return a BundleComposition, that looks something like this:
interface BundleComposition { bundleNames: string[] reducers: { [key: string]: Reducer } selectors: { [key: string]: Selector } reactors: { [key: string]: Reactor } actions: { [key: string]: AnyAction } initMethods: Array<(store: Store) => void> args: Array<{ [I in keyof any[]]: ArgGenerator<any> }[number]> middleware: MiddlewareGenerator[] processed: Bundle[] } Problem I am having is, well twofold, so lets tackle them one by one
1. The Error Issue with generics/default values
When defining this function, we'd define it a function that takes in multiple Bundles and returns a BundleComposition, thus something like this would work:
type ComposeBundles = (...bundles: Bundle[]) => BundleComposition Note that when defining this function, it is impossible to define what "shape" each of these bundles is, precisely, we know they must be a bundle, but Bundle type can, and most definitively should/will have it's type-arguments defined when creating it, however this function is used on multiple different bundles and thus we cannot define the shape of this "array" it accepts, because they are both unknown, and not the exact same shape.
Now, when we define a bundle, like such:
interface ICFG { tag: 'testconfig' } interface IActExt { specificTag: number } const INITIAL_STATE = { testState: 0, } // a simple typeguard const isSpecificAction = (action: any): action is IActExt => !!action.specificTag const ExampleBundle: Bundle<typeof INITIAL_STATE, { testarg: 'success' }, IActExt> = { name: 'testbundle', actions: { testAction: async (a, b) => { }, }, init: store => { console.log('initializing store') console.log(store) }, args: store => { console.log('passing in extra args') // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { testarg: 'success', } }, middleware: composition => store => next => action => { console.log('triggered middleware for action: ', action) if (isSpecificAction(action)) console.log(action.specificTag) else next(action) }, reducer: (state = INITIAL_STATE, { type }) => { if (type === '@CORE/INIT') return { ...state, testState: state.testState + 1, } return state }, } This is a valid bundle, there is no errors thrown by the TSC, it's generics are well defined, but it is impossible to use this bundle as argument of the previously mentioned function, when you try to do the following, an error occurs:
composeBundles(ExampleBundle) Error Message:
Argument of type 'Bundle<{ testState: number; }, { testarg: "success"; }, IActExt>' is not assignable to parameter of type 'Bundle<any, object, object>'. Types of property 'middleware' are incompatible. Type 'MiddlewareGenerator<IActExt> | undefined' is not assignable to type 'MiddlewareGenerator<object> | undefined'. Type 'MiddlewareGenerator<IActExt>' is not assignable to type 'MiddlewareGenerator<object>'. Type 'object' is not assignable to type 'IActExt'.(2345) And this error confuses me, because if you pay close attention, I am attempting to pass a VERY DEFINED BUNDLE into a function that expects a matching, albeit slightly different SHAPE as an argument, yet the error is saying I am doing the opposite. I read that object is not assignable to type IActExt where I've never assigned that, I did assign it the other way around no? What am I missing here? If a function expects a Bundle with a generic value equating object and you pass a Bundle with a generic of T where T extends object is that not suppose to work? The T is an extension of an object by my logic and everything I know about the whole SOLID/OOP shenanigans, this should work.
2. The whole "array" is not "really an array" problem
Truth be told, what we are dealing with in the function mentioned in issue 1 is not an "array", per say. It is as we can see a spread ("...") of multiple arguments, each of which is defined as a specific Bundle and the order of which is very well known because we are calling a function with arguments in a specific order, thus, we are dealing with a Tuple not an Array, but there is no way to define it as such because we don't know what the arguments will be once the function is invoked, nor how many will we have.
Essentially the issue is, we have defined the types:
type T<G extends object = object> = G // for simplicity, its obviously more then this type myObjectWrapper = { subObjects: T[] } type myFunction = (...args: T[]): myObjectWrapper type T1 = T<{a: string}> type T2 = T<{b: string}> And then we implement the "myFunction" and expect to get the Result to be related to the input values of arguments, and the type-system should be aware of this, maybe not inside the body of the function (implementation), but certainly should be aware of it as a result of invocation.
const example: myFunction = (...args) => { // ...implementation stuff return { subObjects: args } } const a: T1 = { a: 'some string' } const b: T2 = { b: 'some other string' } const myResult = example(a, b) // doesn't work properly, type information is lost So what is a proper pattern for defining these functions that accept an "array" of values, be it as an argument spread or an array if that makes it better somehow, where each value must be of some type T<G> but the types of G are different. This function returns an object wrapped around the values taken. How do we write this properly?
Because I find using a simple T[] doesn't work, yet I cannot specify a G because that could be anything that extends an object, which also forces me to define a "default" for the value G so I just default to object, but then I get errors from "issue 1" above.