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
- If the object has the property, accessing it gives the correct type
- 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.