2

Let's say the following object is created using the specified type definition. It must use the index signature [key: string] as the object is allowed to have any keys, or even no key at all.

interface CreateObject {
    [key: string]: {
        foo: string
        bar: number
    }
}

const myObject: CreateObject = {
    fooKey: {
        foo: "something",
        bar: 1
    },
    barKey: {
        foo: "something else",
        bar: 2
    }
}

Now, let's say I want to create a function that takes in a key parameter. The value of key should be equal to an actual key value within myObject, i.e. in the example above key should only ever be equal to fooKey or barKey.

interface SomeFunction {
    (key: keyof typeof myObject): void
}

const someFunction: SomeFunction = (key) => {
    console.log(myObject[key].foo)
}

This won't work as keyof typeof myObject is equal [key: string], therefore as long as key in someFunction(key) is equal to any string then it won't have any type errors.

How do I ensure that only the actual keys within myObject can be passed in as the key parameter? For example:

someFunction("fooKey") // should pass
someFunction("barKey") // should pass
someFunction("notAValidKey"); // should fail

Here's a Playground link demonstrating the issue.

1 Answer 1

3

Don't widen the type of your object to your interface with an index signature. Once you do that it's impossible to know what keys are on your object or not.

If you still need to enforce the type of myObject, you can wrap it in an identity function that checks its type without widening it.

The createObject() wrapper is not strictly necessary, but makes the nature of the error easier to identify when it is generated at the instantiation rather than the usage of the object. If you remove the call to createObject() and introduce a typo into the definition of myObject, you'll still get an error but it will occur at its usage in the calls to someFunction().

interface CreateObject {
    [key: string]: {
        foo: string
        bar: number
    }
}

function createObject<T extends CreateObject> (object: T) {
    return object
}

const myObject = createObject({
    fooKey: {
        foo: "something",
        bar: 1
    },
    barKey: {
        foo: "something else",
        bar: 2
    }
})

interface SomeFunction {
    <T extends CreateObject> (object: T, key: keyof T): void
}

const someFunction: SomeFunction = (object, key) => {
    console.log(object[key].foo)
}

someFunction(myObject, 'fooKey') // passes
someFunction(myObject, 'barKey') // passes
someFunction(myObject, 'notAValidKey') // fails
Sign up to request clarification or add additional context in comments.

15 Comments

If I don't widen with type with an index signature then I have no type safety for myObject. For example, I could set barKey: { oof: "something else" } (misspelling) and it wouldn't detect any error.
I saw you edited your answer but I'm not sure how that fits into the code example.
OK I think I understand what you mean. Unfortunately, my real life example is a lot more complex than this and this solution won't work. To put it very simply, const someFunction is defined within a separate npm package I'm creating, and myObject is defined at the project root by the user. Therefore I need type safety as well as auto-completion/suggestions for myObject directly so that users receive warnings/errors if they try defining myObject incorrectly.
@GCM the keyof typeof myObject in your interface definition wouldn't be possible in that case anyway. How were you planning on defining that signature?
@Daniel again I refer you to createStyles. Using an identity function to prevent type widening is a very common pattern, and with something as generic as interface Create<T> { <V extends T>(v: V): V }, you can use it to perform type checking on literally any type. If there was a way to do this without functions, large libraries like material-ui would definitely be using it instead, but there isn't.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.