I have a type of object that contains some functions. Each of these functions takes an args object. Every such function is similar except:
Some have known args structures
Some have unknown args structures
This playground link lays it out more clearly, shows what I have so far, and then gets to the problem I'm having.
// There's a prop required in every args interface
interface ArgsBaseInterface {
someRequiredProp: string;
}
// Sometimes we don't know what the args interface will look like, other than that it will have the prop required from the base interface
interface UnknownArgsInterface extends ArgsBaseInterface {
[index: string]: any;
}
type UnknownFunc = (args: UnknownArgsInterface) => void;
interface FuncsWithUnknownArgs {
[index: string]: UnknownFunc;
}
// We can build an object of these funcs, just like we want
const someObject: FuncsWithUnknownArgs = {
someFunc: (args: UnknownArgsInterface) => {console.log(args.someArg())},
someOtherFunc: (args: UnknownArgsInterface) => {console.log(args.someOtherArg())}
}
// But some of the functions have known interfaces
interface KnownInterfaceOne extends ArgsBaseInterface {
someString: string;
someNumber: number;
}
interface KnownInterfaceTwo extends ArgsBaseInterface {
someBool: boolean;
someStringArray: string[];
}
interface KnownInterfaceThree extends ArgsBaseInterface {
someNumber: number;
someNumberArray: number[];
}
// We can use a map to associate known functions with known interfaces
interface KnownFuncMap {
knownFuncOne: KnownInterfaceOne;
knownFuncTwo: KnownInterfaceTwo;
knownFuncThree: KnownInterfaceThree;
}
// Using a mapped type, this works great
// (Has to be a type alias since interfaces don't allow for these kinds of indexes)
type FuncsWithKnownArgs = {
[K in keyof KnownFuncMap]?: ((args: KnownFuncMap[K]) => void);
};
// Object contains some known funcs, and any that appear in the object need to use the corresponding args interface - great!
const someOtherObject: FuncsWithKnownArgs = {
knownFuncTwo: (args: KnownInterfaceTwo) => {console.log(args.someBool)},
knownFuncThree: (args: KnownInterfaceOne) => {console.log(args.someNumber)} // if args interface doesn't match, we get a type error - great!
}
// But what if I want both?
// I have an object filled with funcs where:
// if the func IS in KnownFuncMap, args MUST BE the corresponding interface from KnownFuncMap
// if the func ISN'T in KnownFuncMap, args is UnknownArgsInterface
// I can't figure out how to do this!
// Nope!
interface KnownAndUnknownFuncs extends FuncsWithKnownArgs {
[key: string]: ((args: UnknownArgsInterface) => void);
}
// This compiles, but it destroys type safety. In fact, now it's possible for args to be a type other than one of the known OR unknown interfaces!
interface KnownAndUnknownFuncsTakeTwo extends FuncsWithKnownArgs {
[key: string]: ((args: any) => void) | undefined;
}