简体   繁体   中英

Typescript - How to access a variable object property from a variable object

I'm having some difficulty trying to write a function that takes two inputs:

  1. The name of an object
  2. The name of a property

and prints the value of that property for that object. However, the objects all have different properties, they're not all the same.

The objects look like this:

class object1 {
  get property1() {
    return 'foo';
  }
  get property2() {
    return 'bar';
  }
}

export default new object1();
class object2 {
  get different1() {
    return 'asdf';
  }
  get different2() {
    return 'ghjk';
  }
}

export default new object2();

Here's what I'm tried so far:

import object1 from '..';
import object2 from '..';

getPropertValue(objectName, propertyName) {
  let objects = [object1, object2];
  let index = objects.indexOf(objectName);
  console.log(objects[index][propertyName]);
}

This didn't work, came back as undefined. It seems like index is being calculated correctly, but it doesn't seem like objects[index][propertyName] is properly accessing the object's value. Though weirdly, when I tried the following, it ALMOST worked:

import object1 from '..';
import object2 from '..';

getPropertValue(objectName, propertyName) {
  let objects = [object1, object2];
  for (let index in objects) {
    console.log(objects[index][propertyName]);
  }
}

Here I actually got it to print the correct value, but the problem is that since I'm just iterating over all the objects in a for loop, it tries to print the value for all objects, instead of just the one that matches the objectName. So it prints the correct value for the object that has the property I'm trying to access, but then gets undefined for the other object which does not have that property.

I suppose I could add some property to each object called name and do something like this:

getPropertValue(objectName, propertyName) {
  let objects = [object1, object2];
  for (let index in objects) {
    if(objects[index].name == objectName) {
      console.log(objects[index][propertyName]);
    }
  }
}

But I'd rather not add unnecessary properties to these objects if I can help it.

Is there a better way to do this?

Objects can be referenced by a variable, but and object has no quality of being named unless your code explicitly provides that as part of your data model.

For instance:

const obj = { abc: 123 }

Here the value { abc: 123 } is not named obj . There is a variable with an identifier of obj that references the value that is { abc: 123 } .

What this means in practice is that if you only have a reference to { abc: 123 } , then you cannot know what other variable identifiers also hold a reference to this value.

And you cannot get a variable's value by its identifier as a string.

So this cannot possibly work:

const foo = 'bar'

function getVar(varName: string) {
  // impossible function implementation here.
}

getVar('foo') // expected return value: 'bar'

But what you can have is an object with properties. Properties have keys which are specific strings.

const objects = { // note the {}, [] is for arrays
  object1,
  object2,
}

Which is shorthand for:

const objects = {
  "object1": object1,
  "object2": object2,
}

You now have object with properties that correspond to the names you want to use for your objects.


Lastly, let's type your function properly:

// store this outside the function
const objects = {
  object1,
  object2,
}

function getPropertyValue<
  T extends keyof typeof objects
>(
  objectName: T,
  propertyName: keyof typeof objects[T]
) {
  console.log(objects[objectName][propertyName]);
}

This function is now generic, which means it accepts a type. In this case it accepts the object name you want to use as T , which any keyof the objects object.

Then the propertyName argument uses whatever key that was to find out what properties should be allowed, by constraining the type to the properties of just one of those objects.

Now to get the data you drill into objects by the name of a property on objects , and then again on the specific property name.

Usage looks like this:

getPropertyValue('object1', 'property1') // 'foo'
getPropertyValue('object2', 'different1') // 'asdf'

getPropertyValue('object1', 'different1')
// Type error, 'different1' does not exist on 'object1'

See Playground

Object names

As Alex Wayne explains : Objects will only have names "as part of your data model."

Or in other words: An object's name is what you associate it with.

That association may come from within or from outside. Example:

// Name by self-definition
const someObject = { name: "self-named" };

// Name by association
const nameToObject = { "associated-name": {} };

Looking up by name

To avoid adding unnecessary properties to your objects, I'll use the "name by association" approach:

const objects = { object1, object2 };

Sidenote : In this shorthand object initialization , the identifiers are used as property names and their references as the properties' values.

With this, an implementation is straightforward:

 const object1 = new class Object1 { get property1() { return "foo"; } get property2() { return "bar"; } }; const object2 = new class Object1 { get different1() { return "asdf"; } get different2() { return "ghjk"; } }; const objects = { object1, object2 }; // The implementation function getPropertyValue(objectName, propertyName) { return objects[objectName][propertyName]; } const value = getPropertyValue("object1", "property2"); console.log(value);

Adding Typescript

To add proper typing, we need to think about how the output depends on the inputs. But also, how the inputs depend on each other.

A naive implementation may look like this:

function getPropertyValue(
  objectName: "object1" | "object2",
  propertyName: "property1" | "property2" | "different1" | "different2"
): any;

But this doesn't fully use what Typescript is capable of. Only a subset of propertyName 's original types can be used depending on objectName 's value: propertyName depends on objectName .

Since there is one dependency between function parameters, we need a generic parameter to link them.

We already know which value of objectName corresponds to which type, thanks to our dictionary. We only need to restrict propertyName to that type's keys:

type Objects = typeof objects;

function getPropertyValue<
  K extends keyof Objects
>(
  objectName: K,
  propertyName: keyof Objects[K]
);

When calling this function, K will be inferred from objectName by default, and automatically restrict our options for propertyName according to our typing.


Addendum

Name by identifier?

While (especially const declared) variables can reference objects, their identifier –that is the variable name– is usually not considered their referenced object's name. For good reason!

You cannot easily look up objects through an identifier. To do so requires either new Function or eval() , which are known for having security issues .

Therefore, please see the following as a demonstration, not recommendation :

 const object1 = { name: "Object-1" }; const object2 = { name: "Object-2" }; const o = getObjectByName("object1"); console.log(o); function getObjectByName(name) { // You should avoid new Function; return new Function(`return ${name};`)(); }

for...in , for...of , what for?

There are multiple for loops:

  • The regular (or simple) for -loop with an initialization, condition and incrementation block.
  • The for...of -loop to iterate through an iterable object.
  • The for...in -loop to loop over enumerable properties .

Make sure you familiarize yourself with these!

Tip : In many cases you can use Object.keys() or .entries() instead of a for...in -loop, which avoids confusion with for...of -loops.

Namespace pollution!

We don't want to instantiate objects for every function call, which means it needs to be outside that function.

But naively moving it outside pollutes the namespace.

Instead, we can make use of a closure to instantiate it only once, but keep it hidden from the global scope:

 // Mock imports const object1 = { property1: "foo", property2: "bar" }; const object2 = { different1: "asdf", different2: "ghjk" }; const getPropertyValue = (() => { const objects = { object1, object2 }; return function(objectName, propertyName) { return objects[objectName][propertyName]; }; })(); const value = getPropertyValue("object1", "property2"); console.log(value); console.log(objects);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM