Skip to content

Commit

Permalink
Merge pull request #311 from gvergnaud/gvergnaud/improve-is-matching-…
Browse files Browse the repository at this point in the history
…predicate

isMatching: improve narrowing
  • Loading branch information
gvergnaud authored Feb 26, 2025
2 parents 4651f69 + d5d1bf2 commit 672b5ef
Show file tree
Hide file tree
Showing 4 changed files with 22 additions and 9 deletions.
3 changes: 2 additions & 1 deletion src/is-matching.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MatchedValue, Pattern, UnknownProperties } from './types/Pattern';
import * as P from './patterns';
import { matchPattern } from './internals/helpers';
import { WithDefault } from './types/helpers';

/**
* This constraint allows using additional properties
Expand Down Expand Up @@ -47,7 +48,7 @@ export function isMatching<const p extends Pattern<unknown>>(
export function isMatching<const T, const P extends PatternConstraint<T>>(
pattern: P,
value: T
): value is P.infer<P>;
): value is T & WithDefault<P.narrow<T, P>, P.infer<P>>;

export function isMatching<const p extends Pattern<any>>(
...args: [pattern: p, value?: any]
Expand Down
2 changes: 1 addition & 1 deletion src/patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export type infer<pattern> = InvertPattern<NoInfer<pattern>, unknown>;
* type Narrowed = P.narrow<Input, typeof Pattern>
* // ^? ['a', 'a' | 'b']
*/
export type narrow<input, pattern extends Pattern<any>> = ExtractPreciseValue<
export type narrow<input, pattern> = ExtractPreciseValue<
input,
InvertPattern<pattern, input>
>;
Expand Down
16 changes: 15 additions & 1 deletion tests/is-matching.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ describe('isMatching', () => {
expect(isMatching({ topping: 'cheese' }, food)).toBe(true);

if (isMatching({ topping: 'cheese' }, food)) {
type t = Expect<Equal<typeof food, Food & { topping: 'cheese' }>>;
type t = Expect<
Equal<typeof food, Pizza & { topping: 'cheese'; type: 'pizza' }>
>;
}
});

Expand All @@ -150,4 +152,16 @@ describe('isMatching', () => {
type t = Expect<Equal<typeof food, Food & { unknownProp: Error }>>;
}
});

it('should correctly narrow undiscriminated unions of objects.', () => {
type Input = { someProperty: string[] } | { this: 'is a string' };
const input = { someProperty: ['hello'] } satisfies Input as Input;

if (isMatching({ someProperty: P.array() }, input)) {
expect(input.someProperty).toEqual(['hello']);
type t = Expect<Equal<typeof input.someProperty, string[]>>;
} else {
throw new Error('pattern should match');
}
});
});
10 changes: 4 additions & 6 deletions tests/objects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,17 @@ describe('Objects', () => {
it('should work with symbols', () => {
const fn1 = (obj: Input) => {
if (isMatching({ [symbolA]: { [symbolB]: 'foo' } }, obj)) {
type t = Expect<
Equal<typeof obj, { [symbolA]: { [symbolB]: 'foo' } }>
>;
const value = obj[symbolA][symbolB];
type t = Expect<Equal<typeof value, 'foo'>>;
} else {
throw new Error('Expected obj to match the foo pattern!');
}
};

const fn2 = (obj: Input) => {
if (isMatching({ [symbolA]: { [symbolB]: 'bar' } }, obj)) {
type t = Expect<
Equal<typeof obj, { [symbolA]: { [symbolB]: 'bar' } }>
>;
const value = obj[symbolA][symbolB];
type t = Expect<Equal<typeof value, 'bar'>>;
throw new Error('Expected obj to not match the bar pattern!');
}
};
Expand Down

0 comments on commit 672b5ef

Please sign in to comment.