Home > other >  How can I get the correct type if I access an object property with a string in TypeScript?
How can I get the correct type if I access an object property with a string in TypeScript?

Time:11-02

Say I have the following code:

interface Response { 'property.a': number; 'property.b': string }
const response: Response = {
  'property.a': 10,
  'property.b': 'foo'
}

I want to access the properties of response in a type-safe way, but it doesn't completely work as I would expect. What I am looking for is a way of accessing object properties with strings, where

  1. If the object has the property, accessing it gives the correct type
  2. If the object does not have the property, accessing it throws an error

If you access the properties directly, this is what happens:

response['property.a'] // works as expected: is of type number
response['property.c'] // should throw an error, but doesn't

So condition 1 is satisfied, but condition 2 isn't. I also tried the following:

const get = <T>(obj: T, key: keyof T) => obj[key]
get(response, 'property.a') // should be of type number, but is number | string;
get(response, 'property.c') // works as expected: throws error

So at least throws the error, but now it returns a union of all possible property types in Response- this method fails on condition 1, but satisfies condition 2.

Is there a method that satisfies both conditions?

EDIT:

I just figured out that there is a TypeScript rule called noImplicitAny that enabled this kind of behavior! Unfortunately I can't enable this rule in the codebase I'm currently working in, so I'll keep this question open in case there's another workaround.

CodePudding user response:

For direct property access you should definitely get an error unless you don't have the --noImplicitAny compiler option enabled, which is part of the "standard" --strict suite of compiler options. It is almost universally recommended to enable --strict.


Taking about your get() function method:

Since key is of type keyof T, then obj[key] will be of the indexed access type T[keyof T], which is going to be a union of all known property types in T.

If you want to keep track of the literal type of the specific key passed in as the key parameter, then your get() function needs to be generic in that type, like this:

const get = <T, K extends keyof T>(obj: T, key: K) => obj[key]

Now key is of generic type K constrained to keyof T, and obj[key] is inferred to be of the indexed access type T[K]. Let's test it:

const num = get(response, 'property.a');
// const num: number

Here, T is inferred to be Response and K is inferred to be "property.a", and so T[K] is number, as desired.

Playground link to code

  • Related