typescriptindex-signature

How to combine known interface properties with a custom index signature?


How do you type an object that can have both a few declared optional properties, e.g.:

{ 
    hello?: string, 
    moo?: boolean 
}

as well as custom properties (that must be functions), e.g.:

    [custom: string]: (v?: any) => boolean

This is what I'd like to see for example:

const myBasic: Example = {moo: false}
// -> ✅ Valid! Using known keys

const myValid: Example = {hello: 'world', customYo: () => true}
// -> ✅ Valid! "customYo" is a function returning a bool. Good job!

const myInvalid: Example = {hello: 'world', customYo: 'yo!'}
// -> ☠️ Invalid! "customYo" must be a function returning a boolean

Trying to add an index signature to an interface with known keys (i.e. hello?: string, moo?: boolean) requires all keys to be subsets of the index signature type (in this case, a function returning a boolean). This obviously fails.


Solution

  • This is not possible, by design https://basarat.gitbooks.io/typescript/docs/types/index-signatures.html

    As soon as you have a string index signature, all explicit members must also conform to that index signature. This is to provide safety so that any string access gives the same result.

    The only way to get around it is to exploit that each interface can have 2 separate index signatures, one for string and number

    In you example hello and moo make the string index unusable, but you can hijack the number index for the custom methods

    interface IExample {
      hello?: string
      moo?: boolean
      [custom: number]: (v?: any) => boolean
    }
    
    const myBasic: IExample = {moo: false}
    // -> ✅ Valid! Using known keys
    
    const myValid: IExample = {hello: 'world', 2: () => true}
    // -> ✅ Valid! "customYo" is a function returning a bool. Good job!
    
    const myInvalid: IExample = {hello: 'world', 2: 'yo!'}
    // -> ☠️ Invalid! "customYo" must be a function returning a boolean
    

    This works but is hardly an acceptable interface as would lead to unintuitive functions and you would have to call them by array notation

    myValid.7() // Cannot invoke an expression whose type lacks a call signature. Type 'Number' has no compatible call signatures.
    myValid[2]() // works (but ewwwww what is this!!!)
    // could alias to more readable locals later but still ewwwwww!!! 
    const myCustomFunc = myValid[2]
    myCustomFunc() // true
    

    This also has the caveat that the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number, javascript will convert the number to a string before indexing into an object

    In this case you have no explicit string indexer, so the string index type is the default any which the numeric indexer type can conform to

    IMPORTANT This is just for the science, I don't recommend this as a real life approach!