Home > Back-end >  How to index functions that return objects from a factory with possible additional properties
How to index functions that return objects from a factory with possible additional properties

Time:01-25

I am trying to refactor my code to abstract redundant pieces to improve maintainability. I'm trying to create a single function that, depending on the parameter passed, will execute different smaller functions and return the necessary data.

For these smaller functions, they return an object whose properties come from a factory function, plus any additional properties I define that may or may not exist between all smaller functions.

const factoryFunc = () => ({
    func1: () => 'func1',
    func2: () => 'func2',
    func3: () => 'func3',
})

const extendedFuncs1 = () => ({
    ...factoryFunc(),
    additionalFunc1: () => 'additionalFunc1'
})

const extendedFuncs2 = () => ({
    ...factoryFunc()
})

I extracted the types return from these functions using the ReturnType utility. I wanted to get the keys for each available function, so I created a type that maps the keys to their respective function name.

type TExtendedFuncs1 = ReturnType<typeof extendedFuncs1>
type TExtendedFuncs2 = ReturnType<typeof extendedFuncs2>

type TFuncsTypes = {
    extendedFuncs1: keyof TExtendedFuncs1;
    extendedFuncs2: keyof TExtendedFuncs2;
}

Then, I created a conditional type to check if the property is of a certain function, and if it is, give the keys available for that function. The OtherType is for example only.

type TOtherTypes = {
    otherType1: string;
    otherType2: number
}

type Conditional<T = keyof (TOtherTypes & TFuncsTypes)> = T extends keyof TFuncsTypes ? {
    name: T;
    objKey: TFuncsTypes[T]
} : never

With this, I expected the objKey property to be the keys of the returned object of either TFuncsTypes['extendedFuncs1'] or TFuncsTypes['extendedFuncs2']. Additionally, if it is the TFuncsTypes['extendedFuncs1'], the additionalFunc1 property should exist.

const testFunc = (data: Conditional[]) => {
    const findData = (key: string) => data.find((d) => d.name === key);

    const res1 = extendedFuncs1()[findData('extendedFuncs1')!.objKey]
    const res2 = extendedFuncs2()[findData('extendedFuncs2')!.objKey]

    return {res1, res2}
}

However, typescript gives me an error for res2

Property 'additionalFunc1' does not exist on type '{ func1: () => string; func2: () => string; func3: () => string; }'

I am aware that it does not exist, as it is an additional property defined outside the factory, but why is it not getting evaluated to the keys defined in TFuncsTypes['extendedFuncs2']?

Here is a playground I made.

CodePudding user response:

When you write the type Conditional[], you are referring to an array of the generic Conditional type where the type arguments take on their defaults:

type C = Conditional;
/* type C = {
    name: "extendedFuncs1";
    objKey: "additionalFunc1" | "func1" | "func2" | "func3";
} | {
    name: "extendedFuncs2";
    objKey: "func1" | "func2" | "func3";
} */

which is therefore a union type. Your findData function therefore returns a value of that union type (or undefined), no matter what key is:

const findData = (key: string) => data.find((d) => d.name === key);

const hmm = findData("extendedFuncs2");
/* const hmm: {
name: "extendedFuncs1";
objKey: "additionalFunc1" | "func1" | "func2" | "func3";
} | {
name: "extendedFuncs2";
objKey: "func1" | "func2" | "func3";
} | undefined */

For all the compiler knows, then, the result will have an objKey property of type "additionalFunc1". And that means it will complain if you try to index into the output of extendedFuncs2().


In order to fix this you need to express the relationship between the key string passed to findData() and the possible output type, so that the compiler understands that, for example, findData("extendedFuncs2")?.objKey cannot be "additionalFunc1".

One way to do this is to make findData() generic in the type K of key, and use the call signature for the find() method of arrays that returns a narrower result if its callback is a custom type guard function:

interface Array<T> {
    // This is the call signature we want to use
    find<S extends T>(
        predicate: (this: void, value: T, index: number, obj: T[]) => value is S,
        thisArg?: any
    ): S | undefined;
}

Like this:

const findData = <K extends string>(key: K) => data.find(
    (d): d is Extract<typeof d, { name: K }> => d.name === key
);

Now when we call findData() we will see more specific results:

const hmm = findData("extendedFuncs2");
/* const hmm: {
name: "extendedFuncs2";
objKey: "func1" | "func2" | "func3";
} | undefined */

The compiler now knows that if objKey exists it will be a valid key of the result of extendedFuncs2(), and your error goes away:

const res2 = extendedFuncs2()[findData('extendedFuncs2')!.objKey]; // okay

Depending on your use case, this might be sufficient.


Playground link to code

  • Related