sindresorhus / is

Type check values
MIT License
1.68k stars 109 forks source link

Proposal: validate type of object properties #141

Open lo1tuma opened 3 years ago

lo1tuma commented 3 years ago

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
  }
}
sindresorhus commented 3 years ago

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

From a quick search, https://github.com/microsoft/TypeScript/pull/38839 looks relevant.

lo1tuma commented 3 years ago

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 https://github.com/microsoft/TypeScript/issues/34974.

younho9 commented 2 years ago

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

younho9 commented 2 years ago

There is a similar implementation in ow. https://github.com/sindresorhus/ow/issues/92