I am looking to create a custom function that mimics the behavior of the in
operator in TypeScript, utilizing user-defined type guards.
(To see an example, check out Lodash's has
function.)
When using n in x where n is a string literal or string literal type, and x is a union type, the "true" branch narrows down to types with either an optional or required property n, while the "false" branch narrows down to types missing that property n.
https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-the-in-operator
I have written tests demonstrating the behavior of the in
operator and want to replicate this behavior in my has
function. How can I define the function so it behaves exactly like the in
operator based on these tests?
declare const any: any;
type Record = { foo: string; fooOptional?: string };
type Union = { foo: string; fooOptional?: string } | { bar: number; barOptional?: string };
{
const record: Record = any;
if ('foo' in record) {
record; // $ExpectType Record
} else {
record; // $ExpectType never
}
if (has(record, 'foo')) {
record; // $ExpectType Record
} else {
record; // $ExpectType never
}
}
{
const union: Union = any;
if ('foo' in union) {
union; // $ExpectType { foo: string; fooOptional?: string | undefined; }
} else {
union; // $ExpectType { bar: number; barOptional?: string | undefined; }
}
if (has(union, 'foo')) {
union; // $ExpectType { foo: string; fooOptional?: string | undefined; }
} else {
union; // $ExpectType { bar: number; barOptional?: string | undefined; }
}
}
{
const unionWithOptional: { foo: string } | { bar?: number } = any;
if ('bar' in unionWithOptional) {
unionWithOptional; // $ExpectType { bar?: number | undefined; }
} else {
unionWithOptional; // $ExpectType { foo: string; } | { bar?: number | undefined; }
}
if (has(unionWithOptional, 'bar')) {
unionWithOptional; // $ExpectType { bar?: number | undefined; }
} else {
unionWithOptional; // $ExpectType { foo: string; } | { bar?: number | undefined; }
}
}
The solution I've attempted for now can be found here:
type Discriminate<U, K extends PropertyKey> = U extends any
? K extends keyof U
? U
: U & Record<K, unknown>
: never;
export const has = <T extends object, K extends PropertyKey>(
source: T,
property: K,
): source is Discriminate<T, K> =>
property in source;
Unfortunately, the last test does not pass as expected.
In essence, my goal is to ensure that my custom has
function validates keys at compile time, which the traditional in
operator falls short on. While I can handle the validation aspect, I'm currently struggling with replicating the narrowing effect carried out by the in
operator.