xoux
xoux

Reputation: 3494

How to write a type guard that takes an object of generic type T and a string parameter and checks that the string is of type keyof T?

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)

Upvotes: 3

Views: 521

Answers (2)

xoux
xoux

Reputation: 3494

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.

Upvotes: 2

Michael Lorton
Michael Lorton

Reputation: 44416

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.

Upvotes: 0

Related Questions