3

I'm probably missing something obvious here, but I've been stuck on this one for a while.

Given the following predicate (I know it's not technically a predicate):

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

I want to do something like this:

const coreProps = ['id', 'module', 'name', 'auth'] as const;
for (const prop of coreProps) {
  if (!hasProp(rawObject, prop)) {
    throw new Error(`Missing property '${prop}'`);
  }
}

However, this doesn't let me use e.g. rawObject.id afterwards. If I manually unroll the loop, it works as expected.

Am I doing something wrong, or is this a limitation of the TypeScript compiler?


I did come up with this alternative:

const hasProps = <Ks extends PropertyKey[], T extends {}> (obj: T, ...props: Ks): obj is T & { [K in Ks[number]]: unknown } => {
  return props.every(prop => hasProp(obj, prop));
}

But I don't like this, because it doesn't return any information about which property is missing (if any).

MTCoster
  • 5,868
  • 3
  • 28
  • 49
  • *"But I don't like this, because it doesn't return any information about which property is missing..."* Couldn't it throw like your loop does? – T.J. Crowder Jun 05 '21 at 11:32
  • Technically yes, but I have `hasProps` in a file of utilities and I'll almost certainly use it elsewhere. Ideally I'd like it to be isolated from that implementation detail. – MTCoster Jun 05 '21 at 11:35
  • 2
    It doesn't seem like an implementation detail to me, it seems like it's intrinsic to what the function does. But you could always make a companion function you use in the false case that tells you what's missing. Anyway, hope you find a solution you like. – T.J. Crowder Jun 05 '21 at 11:37
  • 1
    You're right, that probably will be suitable for my use case. I guess now I'm just curious why lifting the loop out of the function causes the type checking to fail – MTCoster Jun 05 '21 at 11:57

1 Answers1

1

I believe you should use assert function for this purpose:

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

const rawObject: any = {}

const coreProps = ['id', 'module', 'name', 'auth'] as const;

type Props = typeof coreProps;


function assert(value: unknown): asserts value is Record<Props[number], unknown> {
  for (const prop of coreProps) {
    if (!hasProp(rawObject, prop)) {
      throw new Error(`Missing property '${prop}'`);
    }
  }
}


assert(arg);// <---- assert function
arg.id // ok

Playground

Assertion function examples

TS official docs

Hence, in order to make it work, you should wrap your loop into assert function. In this way, TS is able to infer the type of your object, otherwise it wont be able to do it because of mutable nature of objects.

  • I tried using `hasProp` in my code and it just caused issues and weird type errors I couldn't understand. Replacing it with `Object.prototype.hasOwnProperty.call` made those errors go away. – Boris Verkhovskiy Nov 14 '22 at 04:39
  • @BorisVerkhovskiy could you please provide me with a reproducible example ? – captain-yossarian from Ukraine Nov 14 '22 at 07:15
  • Here: https://www.typescriptlang.org/play?ssl=20&ssc=1&pln=21&pc=1#code/GYVwdgxgLglg9mABACwIYGcA8AVANIgaUQFMAPKYsAE3UQAUAnOAB2IagE8DiOA+ACgBQiRHABGAKwBciPMMTMmzGQUEBKGeImIYtbIgBkiAErEIcBlUwF84ANZg4AdzC9EAb3kNiUEAyQA8pJmUAB0inBQkRysoWjoAS6MLGycoRCoADaZ-Fr4EcxqANyCAL6CgqCQsAgKIOjIycw4McQC8loy7ogA2nY8MuhQDDBgAOYAujLYrT0TiKW48gWDw6NjSyIAblkgxNOt6h7yMMCI-ACE8bmS+UpqasciIlo9BfMAvL0TJSLlL5I3koJuF6sh+DtMntil4fH4kFoSuVKuBoPAkMwwU0AHIWAC2WRarHaAOkHl6-Q4qxG4ymslm80WyyU1PWm0QkL2B1YR08IlO5wuQQkIXCTCinFi8USYCaqQ46SyOTyCnujz5z1e70QXzmvwWHUB71BDQhu2IMJE3l8-lEkiRgiAA – Boris Verkhovskiy Nov 14 '22 at 07:42
  • 1
    @BorisVerkhovskiy `has` acts as a typeguard, it works flawless without negation, but with negation, TS is unable to infer `obj`, because there is no such thing as a `type negation` in typescript. Since you are mutatinh the object, you can consider this `Object.assign(obj, { [prop]: [] })` – captain-yossarian from Ukraine Nov 14 '22 at 08:15