2
votes

I have an object that is of type MyObject, and it has two string properties.

interface MyObject {
  a: number,
  b: string,
}

const myObject = {
  a: 5,
  b: 'str'
}

Then I have a function that takes a string, and I want to be able to access the property on the aforementioned object specified by the string parameter. What I am trying to do is use a type guard to check if the string is a key of the object, before I access the property. It is necessary to make some kind of check here because the parameter is just a string and the object doesn't have an index signature.

If I make a specific version to check for this specific type of object (MyObject) it works:

// specific version
const isValidPropertyForMyObject = (property: string): property is keyof MyObject => Object.keys(myObject).indexOf(property) !== -1

const getProperty1 = (property: string) => {
  if (isValidPropertyForMyObject(property)) {
    myObject[property]
  }
}

However, what if I want to be able to pass in an object with a generic type, and a string parameter, and check that the property is in fact a key of the object? Here is my attempt:

const isValidMethodForHandler = <T extends { [i: string]: any }>(handler: T) => (
  method: string
): method is keyof T => Object.keys(handler).indexOf(method) !== -1;


const getProperty = (property: string) => {
  // const acceptedProperties = ["a", "b"];
  // if (acceptedProperties.indexOf(property) !== -1) {
  //   myObject[property]
  // }

  if (isValidMethodForHandler(myObject)(property)) {
    myObject[property]
  }
}

The issue is in the type guard:

A type predicate's type must be assignable to its parameter's type. Type 'keyof T' is not assignable to type 'string'. Type 'string | number | symbol' is not assignable to type 'string'. Type 'number' is not assignable to type 'string'.(2677)

2
Is there any reason for specifically wanting a user defined type guard here? Using the in operator is much less verbose: if (key in obj) { return obj[key] }wrsx
@wrsx I don't thing that solution works, I just tried it in TS Playground, as follows: const getProperty3 = (property: string) => { if (property in myObject) { myObject[property] } }evianpring
With no index signature its not possible, but you could enforce an index signature with a generic like in your example: getProperty<T extends { [i: string]: any }>(property: string, obj: T)wrsx
I see no difference: codesandbox.io/s/vigilant-hoover-b3v4hwrsx
@wrsx The difference is that I need a type guard. In other words, I want to be able to refine the type of the argument passed in. Your solution simply gets the property from the object, and returns undefined if the property is not in the object. That is pointless, and could be done with a simple obj[property] directly in the calling code.evianpring

2 Answers

1
votes

The answer is based on this thread in TypeScript issues tracker here.

An explanation of the specific TypeScript error in the question above is in this other question

The solution for my example code is:

const isValidMethodForHandler = <T extends { [i: string]: any }>(handler: T) => (
  method: string
): method is Extract<keyof T, string> => Object.keys(handler).indexOf(method) !== -1;

const getProperty = (property: string) => {
  if (isValidMethodForHandler(myObject)(property)) {
    myObject[property]
  }

keyof returns all known keys, and these are of type string | number | symbol.

To get only the string properties, use Extract.

0
votes

You could certainly write:

interface MyObject {
  a: number,
  b: string,
}

const myObject = {
  a: 5,
  b: 'str'
}
const isValidPropertyForMyObject = (property: string): property is keyof MyObject => 
     property in myObject;

Then you could do things like:

const f = <K extends keyof MyObject>(obj: MyObject, k: K): MyObject[K] => obj[k];

const g = <K extends keyof MyObject>(obj: MyObject, k: string | K): MyObject[K] | string =>
    isValidPropertyForMyObject(k) ? f(obj, k) : "NOT LEGAL";

That assumes there is a constant myObject that at runtime lists all the keys of MyObject.