sindresorhus / is

Type check values

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Proposal: validate type of object properties

lo1tuma opened this issue · comments

I would like to propose a new function is.propertyOf(object: unknown: key: string, predicate: Predicate): boolean that accepts 3 values and checks:

  1. wether the given object value is an object and
  2. if the given object has an own property with the name key and
  3. that the value of the specified property matches the given predicate

Example Usage:

is.propertyOf(foo, 'bar', is.string);

Why

Given the following example

interface Foo {
  bar?: string;
  baz: number
}

function doStuff(foo?: Foo) {
  if (is.string(foo?.bar)) { // using optional chaining because `foo` might be undefined
    console.log(foo.baz); // typescript still things `foo` could be undefined
  }
}

Unfortunately typescript is not smart enough to understand that within the if block foo is always defined.

Let’s say is.propertyOf is implemented similar to this:

function propertyOf<O extends unknown, K extends keyof Exclude<O, undefined>, P>(obj: O, key: K, predicate: Predicate<P>): obj is Exclude<O, undefined> & Record<K, P> {
  return true;
} 

then the code from above could look like this:

interface Foo {
  bar?: string;
  baz: number
}

function doStuff(foo?: Foo) {
  if (is.propertyOf(foo, 'bar', t.string)) {
    console.log(foo.bar); // typescript now knows two things: foo is an object and its property bar is a string
  }
}

Are you aware of any TypeScript issues about it? Would you be able to link some?

From a quick search, microsoft/TypeScript#38839 looks relevant.

I haven’t really checked the TypeScript issues before. It looks like that microsoft/TypeScript#38839 could fix this problem. I’ve also found this issue microsoft/TypeScript#34974.

It seems likely to be extended to a function that verify the schema of an object.

Some ideas are as follows.

import is from '@sindresorhus/is'
import {objectEntries, objectHasOwn} from 'ts-extras';

const isInterface = <ObjectType extends Record<string, unknown>>(
  value: unknown,
  interface_: {
    [Key in keyof ObjectType]: (value: unknown) => value is ObjectType[Key];
  },
): value is ObjectType => {
  return objectEntries(interface_).every(
    ([key, predicate]) => objectHasOwn(value, key) && predicate(value[key]),
  );
};

declare const someObject: unknown;

if (
  isInterface(someObject, {
    foo: is.string,
    bar: is.number,
    baz: is.boolean,
  })
) {
  someObject;
  // {
  //     foo: string;
  //     bar: number;
  //     baz: boolean;
  // }
}

TypeScript Playground

There is a similar implementation in ow. sindresorhus/ow#92