Type a "type predicate" without any

Viewed 573

I have an issue trying to properly type one of my function. In the following code, the type of x is any and I would like to type it better than that.

interface Pet {
  name: string;
}

const checkType = (x: any): x is Pet => {
  return 'name' in x && typeof x.name === 'string';
}

I figure it out that unknown or object would be best fitted, but both give me an error as well

interface Pet {
  name: string;
}

const checkType = (x: unknown): x is Pet => {
  return 'name' in x && typeof x.name === 'string';
}

Object is of type 'unknown'

interface Pet {
  name: string;
}

const checkType = (x: object): x is Pet => {
  return 'name' in x && typeof x.name === 'string';
}

Property 'name' does not exist on type 'object'


So my question is, how can I properly type x without casting to any ?



The following could be a soluce but I find it too much and specific :

playground

interface Pet {
  name: string;
}

const checkType = (x: object): x is Pet => {
  return 'name' in x && typeof (x as {
    name: unknown,
  }).name === 'string';
}

More infos :

Example with any that could cause an issue :

enter image description here

2 Answers

I would recommend resolving this in one of two ways:

  1. Define a union of the possible input types
  2. Define a key/value type

The first is the approach taken by the example in the Typescript Handbook (which is where I assume you got 'Pet' from:

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

However if you're working on a general library this fails because you won't know what your consumers will feed you and expanding that union is not a good reason for a breaking change. You can then take tack #2:

interface IPojo {
  [key: string]: any,
}

const checkType = (x: IPojo): x is Pet => {
  return 'name' in x && typeof (x as {
    name: unknown,
  }).name === 'string';
};

function foo(p: Pet) {
  console.log(p.name);
}

const bar: IPojo = {}
const baz = 'name';
bar[baz] = 'hi';

foo(bar); // TypeError, compiler can't verify bar.name
if (checkType(bar)) {
  foo(bar); // No TypeError, type narrowed correctly by your guard
}

Playground

I would use a partial of your Pet interface as the input for your type guard.

type maybePet = Partial<Pet>

const checkType = (x: maybePet): x is Pet => {
  return 'name' in x && typeof x["name"] === 'string';
}

This allows for the compiler flagging non object-like inputs like a number for example.

let a = {}; 
if(checkType(a))//allows the type check but will not pass
{
  console.log("a")
} 

let b = {name:"good", other:1}; 
if(checkType(b))//allows the type check and passes
{
  console.log("b")
} 
checkType(3) //typescript complier complains

My example code is a little contrived, if you change the keys of an object you are checking so that they don't match the interface of Pet the compiler complains since it has inferred your object can't be of type Pet, however if you are receiving an any object (or explicitly cast it to any) then the check can proceed.

let c = {namey:"good", other:1}; 
if(checkType(c))//compiler complains because it knows typeof c can't be a Pet
{
  console.log("c")
} 

let d:any = {namey:"good", other:1}; 
if(checkType(d))//allows the type check but will not pass
{
  console.log("d")
} 

Playground Link

Related