How to access typescript object property from a string? [duplicate]

Solution 1:

I don't think there's anything more concise that the compiler can verify as type safe. You can manually enumerate the possibilities like this:

if (input === "foo" || input === "bar" || input === "baz") {
    console.log(o[input]); // okay
}

but you'll find that trying to make that less redundant will only lead to more errors:

if ((["foo", "bar", "baz"] as const).includes(input)) { } // error!
// -----------------------------------------> ~~~~~
// Argument of type 'string | null' is not assignable to parameter
// of type '"foo" | "bar" | "baz"'.

(See this question for more info, and also microsoft/TypeScript#36275 for why this wouldn't even act as a type guard).

So I won't suggest doing this in general (but see below).


There is an open suggestion at microsoft/TypeScript#43284 to allow k in o to act as a type guard on k, in addition to the current support for it to act as a type guard on o (see microsoft/TypeScript#10485). If that were implemented, your original check (input && input in o) would just work with no errors.

The issue in GitHub is currently open and marked as "Awaiting More Feedback"; so if you want to see this happen sometime, you might want to go there, give it a 👍, and describe your use case if you think it's particularly compelling.


Personally I think the best solution here is probably your user-defined type guard function with the s is keyof typeof o type predicate, because it explicitly tells the compiler and any other developer that you intend s in o to narrow s in this way.


Note that because object types are extendible, both your custom type guard and the proposed automatic type guarding from microsoft/TypeScript#43284 are technically unsound; a value of type typeof o may well have properties not known about, since object types in TypeScript are extendible or open:

const p = {
    foo: 1,
    bar: 2,
    baz: 3,
    qux: "howdy"
};
const q: typeof o = p; // no error, object types are extendible

function isKeyOfQ(s: string): s is keyof typeof q {
    return s in q;
}

if (input && isKeyOfQ(input)) {
    input // "foo" | "bar" | "baz"
    console.log(q[input].toFixed(2)); // no compiler error
    // but what if input === "qux"?
}

Here, the compiler sees q as having the same type as o. Which is true, despite there being an extra property named qux. isKeyOfQ(input) will erroneously narrow input to "foo" | "bar" | "baz"... and therefore the compiler thinks q[input].toFixed(2) is safe. But since q.qux that property value is of type string while the others are of type number, there is danger lurking.

In practice this kind of unsoundness isn't a showstopper; there are some intentionally unsound behaviors in TypeScript where convenience and developer productivity is considered more important.

But you should be aware of what you're doing, so that you only use this kind of narrowing in situations where the provenance of your object is known; if you get q from some untrusted source, you might want something more provably sound... such as input === "foo" || input === "bar" || input === "baz" or some other user-defined type guard dealing implemented via ["foo", "bar", "baz"].includes(input):

function isSafeKeyOfQ(s: string): s is keyof typeof q {
    return ["foo", "bar", "baz"].includes(s);
}

if (input && isSafeKeyOfQ(input)) {
    console.log(q[input].toFixed(2)); // safe
}

Playground link to code

Solution 2:

I believe this is the kind of approach you are after...

const o = {
  foo: 1,
  bar: 2,
  baz: 3,
} as const;

function printRequestedKey<Lookup extends Readonly<object>>(lookup: Lookup){
  const input = prompt("Enter the key you'd like to access");
  if (input !== null && input in lookup) {
    console.log(lookup[input as keyof typeof lookup]);
  }
}

printRequestedKey(o);

As per Aadmaa's answer it adds const to your definition of o. It also introduces a Generic binding so that if you're doing anything other than console logging, then the returned value can be some projection of the source object. The javascript guard input in lookup along with the use of Readonly object gives me confidence that the explicit cast input as keyof typeof lookup can't introduce runtime errors. I added a null check because prompt can return null (maybe via a key escape?).