Proposal: validate type of object properties
lo1tuma opened this issue · 4 comments
I would like to propose a new function is.propertyOf(object: unknown: key: string, predicate: Predicate): boolean
that accepts 3 values and checks:
- wether the given object value is an object and
- if the given object has an own property with the name
key
and - 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;
// }
}
There is a similar implementation in ow
. sindresorhus/ow#92