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!