34

The code

const obj = {};
if ('a' in obj) console.log(42);

Is not typescript (no error). I see why that could be. Additionally, in TS 2.8.1 "in" serves as type guard.

But nevertheless, is there an way to check if property exists, but error out if the property is not defined in the interface of obj?

interface Obj{
   a: any;
}

I'm not talking about checking for undefined...

cdbeelala89
  • 2,066
  • 3
  • 28
  • 39

3 Answers3

31

You don't get an error because you use a string to check if the property exists.

You will get the error this way:

interface Obj{
   a: any;
}

const obj: Obj = { a: "test" };

if (obj.b)          // this is not allowed
if ("b" in obj)     // no error because you use string

If you want type checking to work for string properties you could add index signatures using this example

Edric
  • 24,639
  • 13
  • 81
  • 91
Kokodoko
  • 26,167
  • 33
  • 120
  • 197
  • 3
    But "if (obj.b)" also disallows undefined. There is a difference between property not existing and it being undefined. I JUST want to check if the property exists in a typesafe way. – cdbeelala89 Apr 07 '18 at 13:24
  • The type system already defines if the property exists or not, so there is no need to check it yourself. Type guards are used when an object can be of multiple types, such as: `const obj: Thing | OtherThing`. – Kokodoko Apr 07 '18 at 14:42
  • No if i have an interface { a?: any; }, I do not know if the property exists. And if I get an object from a server I don't control (e.g.), I never know what property exists. I just want to discriminate in a typesafe way between actual undefined or property not existing at all! – cdbeelala89 Apr 07 '18 at 16:06
  • 1
    If you don't know what your JSON data looks like, you can use `console.log(o.hasOwnProperty("a"))` to see if a property exists at all. If it exists but it's undefined, then it will still return true. – Kokodoko Apr 07 '18 at 16:28
  • 3
    still is not a typesafe check but your answer is stell the best because only one. and i can always write a function to do a typesafe "in" check myself (with typescript keyof syntax) – cdbeelala89 Apr 15 '18 at 12:51
  • I should add an example of index signatures to get type safety with `obj[“test”]` – Kokodoko Apr 15 '18 at 18:52
  • Very useful to verify interfaces! https://basarat.gitbook.io/typescript/type-system/typeguard#in – Jose Jul 20 '20 at 01:39
12

The following handle function checks hypothetical server response typesafe-way:

/**
 * A type guard. Checks if given object x has the key.
 */
const has = <K extends string>(
  key: K,
  x: object,
): x is { [key in K]: unknown } => (
  key in x
);

function handle(response: unknown) {
  if (
    typeof response !== 'object'
    || response == null
    || !has('items', response)
    || !has('meta', response)
  ) {
    // TODO: Paste a proper error handling here.
    throw new Error('Invalid response!');
  }

  console.log(response.items);
  console.log(response.meta);
}

Playground Link. Function has should probably be kept in a separate utilities module.

quasiyoke
  • 709
  • 9
  • 21
  • 1
    This is not type-safe though. If you define `response: {'a': number}`, `has('items', response)` does not yield an expected compile-time error. – JoannaFalkowska Aug 05 '20 at 10:41
  • 2
    Erm... I've published the solution for cases when you're unable to control the type of arguments of the function `handle`. For example, this could be API endpoint handler -- everyone can accidentally send invalid data there. That's why `handle` accepts the argument of type `unknown` (which means "everything, I don't know what"). Type-safety here means that you're unable (in compile time) to use `response` without checking its type. E.g. [here](https://cutt.ly/qdS5nvj) in line 24 `response.somethingElse` was used without type-check and TypeScript complains about that. – quasiyoke Aug 06 '20 at 11:14
  • Oh, so you meant "type-safe" in a completely different way than I thought. Cool one, thanks for the response! – JoannaFalkowska Aug 07 '20 at 16:35
7

You can implement your own wrapper function around hasOwnProperty that does type narrowing.

function hasOwnProperty<T, K extends PropertyKey>(
    obj: T,
    prop: K
): obj is T & Record<K, unknown> {
    return Object.prototype.hasOwnProperty.call(obj, prop);
}

I found this solution here: TypeScript type narrowing not working when looping

Usage:

const obj = {
    a: "what",
    b: "ever"
} as { a: string }

obj.b // Type error: Property 'b' does not exist on type '{ a: string; }'

if (hasOwnProperty(obj, "b")) {
    // obj is no longer { a: string } but is now
    // of type { a: string } & Record<"b", unknown>
    console.log(obj.b)
}

The limitation with this approach is that you only get a Record back with the single key added that you specified. This might be fine for some needs, but if you need a more general solution then I suggest a library like Zod which can validate a complex object and give you the full type: https://github.com/colinhacks/zod

Sámal Rasmussen
  • 2,887
  • 35
  • 36
  • 3
    I'm baffled that the built-in property checks don't already narrow types like this. I'm adding this to my default "include in all my TypeScript projects" code. – derpedy-doo Mar 17 '22 at 18:20
  • Can you explain what `obj is T & Record` means please? – Boris Verkhovskiy Mar 29 '22 at 08:48
  • I keep getting `Type '' is not assignable to type 'never'` errors when I do `if (!has(o, p) { o[p] = x }` when using your signature instead of `const has = (obj: { [key: string]: any; }, prop: string) => {` which is what I had before. I have to keep doing `(o[p] as Whatever) = x` – Boris Verkhovskiy Mar 29 '22 at 09:07
  • Docs for [type predicates](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates), [intersection types](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types), and the [record type](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type) – Sámal Rasmussen Jul 19 '22 at 14:02