1

I have simple object

const trans = {
a: {
    valueA: 'a'
},
b: {
    valueB: 'b'
}

};

and when i wanna grab a dynamic value of trans.a.valueA I can do:

console.log(trans['a'].valueA);

but when I try wrapp this in function like this:

function foo(key:keyof typeof trans) {
return trans[key];
}

and try get a value of 'valueA'

console.log(foo('a').valueA);

I get a TS error

Property 'valueA' does not exist on type '{ valueB: string; }'

So How I can get dynamic value from object by function? Cheers

2 Answers 2

1

The problem is that keyof typeof trans is equal to the union "a" | "b".

All the compiler knows about foo() is that it takes a value of type "a" | "b", and returns the type of indexing into trans with that.

Thus the output type of foo() is going to be (typeof trans)["a" | "b"])

which is the same as (typeof trans)["a"] | (typeof trans)["b"], or

{ valueA: string; } | { valueB: string; }, which is itself a union.

And if I give you a value of that type, you can't just get its valueA property without making sure that it has one first. That's the warning you're getting... the compiler is telling you that the output of foo() might be { valueB: string; } which doesn't have a valueA property.

So you could check it yourself to make the compiler happy:

const fooA = foo("a");
if ("valueA" in fooA) {
    // fooA is now known to be {valueA: string}
    console.log(fooA.valueA) // okay now
} else {
    // fooA is now known to be {valueB: string}
    throw new Error("THE WORLD MAKES NO SENSE");
}

Fine, everything is good now and we're all set here, right? Just kidding.


You're probably objecting that of course if you pass "a" into foo() that the output will have to be of type {valueA: string}. And you're right, but the compiler doesn't automatically try to verify that. The way to get the behavior you're looking for is to use a generic function:

function foo<K extends keyof typeof trans>(key: K) {
    return trans[key];
}

In that, K is a generic type which must be a subtype of "a" | "b". In practice, that means that K can be either the type "a", or it can be the type "b", or it can possibly be the full union "a" | "b". The return type of foo() is now (typeof trans)[K], a lookup type.

When you call foo() now, the compiler will try to infer the narrowest type for K that it can, and the return type will be the narrowest type it can be as a result.

// K inferred as "a":
const fooA = foo("a");
// fooA is of type (typeof trans)["a"], which is {valueA: string}
console.log(fooA.valueA); // okay

And for "b",

// K inferred as "b":
const fooB = foo("b");
// fooB is of type (typeof trans)["b"], which is {valueB: string}
console.log(fooB.valueB); // okay

And, to demonstrate a legitimate need for checking union types:

// K inferred as the full union "a" | "b" 
const fooAB = foo(Math.random() < 0.5 ? "a" : "b"); 
// fooAB is of type (typeof trans)["a" | "b"], the wide type from before
// better check it ourselves:
if ("valueA" in fooAB) {
    // fooAB is now known to be {valueA: string}
    console.log(fooAB.valueA) // okay
} else {
    // fooAB is now known to be {valueB: string}
    console.log(fooAB.valueB) // okay
}

Whew. Okay, hope that helps. Good luck!

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, it is perfect answer of course works like a rock!
0

The simplest way:

  function foo(key:keyof typeof trans) {
    return trans[key] as any;
  }

1 Comment

yeah I know this way, but it is dirty and I lose hints in editor.. This way do JavaScript from TypeScript.

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.