I'm creating a mapping like function that is going to turn an object like this:
const configObject: ConfigObject = {
a: {
oneWay: (value: string) => 99,
otherWay: (value: number) => "99"
},
b: {
oneWay: (value: number) => undefined,
otherWay: () => 99
}
}
into:
{
foos: {
a: {
convert: (value: string) => 99,
},
b: {
convert: (value: number) => undefined
}
},
bars: {
a: {
deconvert: (value: number) => "99",
},
b: {
deconvert: () => 99;
}
}
}
The issue I'm having is around enforcing the function parameter and return types, based on the ConfigItem's signatures.
The way I'm doing it looks like this:
interface ConfigItem<P, Q> {
oneWay: (value: P) => Q;
otherWay: (value: Q) => P;
}
type ConfigObject = Record<string, ConfigItem<any, any>>; //This is right, I believe.
// any is explicitly an OK type for the ConfigItems to have.
interface Foo<A, B> {
convert: (a: A) => B;
}
interface Bar<A, B> {
deconvert: (b: B) => A;
}
interface MyThing<T extends ConfigObject> {
foos: Record<keyof T, Foo<any, any>> //These are wrong - they should use the types as defined by the config object
bars: Record<keyof T, Bar<any, any>>
}
I later implement a function to create a MyThing like:
function createMyThing<T extends ConfigObject>(configObject: T): MyThing<T> {
//I would use Object.entries, but TS Playground doesn't like it.
const keys = Object.keys(configObject);
return {
foos: keys.reduce((acc, key) => {
return {
...acc,
[key]: {
convert: configObject[key].oneWay
}
}
}, {} as Record<keyof T, Foo<any, any>>), //Again problematic 'any' types.
bars: keys.reduce((acc, key) => {
return {
...acc,
[key]: {
deconvert: configObject[key].otherWay
}
};
}, {}) as Record<keyof T, Bar<any, any>>
};
}
Now this code works:
const configObject: ConfigObject = {
a: {
oneWay: (value: string) => 99,
otherWay: (value: number) => "99"
},
b: {
oneWay: (value: number) => undefined,
otherWay: () => 99
}
}
const myThing = createMyThing(configObject);
console.log(myThing.foos.a.convert("hello"));
console.log(myThing.foos.b.convert("hello")); //No type enforcement!
But we don't have any type enforcement, due to those any statements.
How would I modify my code to make this work?
In Typescript it is possible to extract type signature from an existing value with
typeof
:Based on
ConfigObject
, you can createMyThing
like:For completeness
createMyThing
may be typed as:Demo
Note: I added a check to see if T[M] value extends ConfigItem just a precaution.
These types will definition can help. The name of the types are arbitrary.
adding an conversion of each object in the
configObject
.updated the
MyThing
interfaceupdated
createMyThing
function.small modification to the function signatures in
ConfigItems
interface to allow empty paramsFirst thing you should consider is to not set
configObject
type toConfigObject
, because you lose the structure of the object. Create concrete interface which extendsConfigObject
instead:In
MyThing
to get rid ofany
s you can extract types fromconfigObject
with combining several TS features:Parameters<T>
- Constructs a tuple type of the types of the parameters of a function type TReturnType<T>
- Constructs a type consisting of the return type of function TIndex types
- With index types, you can get the compiler to check code that uses dynamic property names. For example, to pick a subset of propertiesMapped Types
- Mapped types allow you to create new types from existing ones by mapping over property typesWith above we extract argument and return types from
oneWay
andotherWay
methods to set intoFoo<A, B>
andBar<A, B>
:TypeScript Playground
P.S. Extracting types from T looks ugly and there can be done some optimizations, I've just written it explicitly for illustration purpose.