0

I have a type of object that contains some functions. Each of these functions takes an args object. Every such function is similar except:

  1. 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;
}
Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • 1
    I'm quite confused about the utility or meaning of `UnknownArgsInterface` here; `someObject.someFunc({ someRequiredProp: "here" });` is a valid call according to the compiler, but it will cause a runtime error. Functions with "unknown args structures" seem very difficult to reason about; how would you possibly call such a function safely? – jcalz Jun 29 '21 at 01:28
  • The answer to the question as asked is that TypeScript doesn't allow "if the func ISN'T in `KnownFuncMap`, args is `UnknownArgsInterface`" logic; there are no "rest index signatures" as requested in [microsoft/TypeScript#17867](//github.com/microsoft/TypeScript/issues/17867), and there are workarounds such as [the answer to this question](https://stackoverflow.com/questions/61431397/how-to-define-typescript-type-as-a-dictionary-of-strings-but-with-one-numeric-i). But even with such workarounds, I'm still not sure the intent, without seeing [how to fix this issue](//tsplay.dev/WzyY4m). – jcalz Jun 29 '21 at 01:34

0 Answers0