Skip to content

Releases: gvergnaud/ts-pattern

v5.8.0

26 Jul 13:52
999128b
Compare
Choose a tag to compare

TS-Pattern v5.8.0 Release Notes

New Feature: .narrow() Method for Deep Type Narrowing

.narrow() gives you fine-grained control over type narrowing of deeply nested union types during pattern matching.

What is .narrow()?

The .narrow() method allows you to explicitly narrow the input type to exclude all values that have been handled by previous patterns. This is especially useful when working with:

  • Deeply nested union types
  • Nullable properties at any nesting level
  • Complex type structures where you need precise type information

When to Use .narrow()

By default, TS-Pattern automatically narrows top-level union types as you pattern match. However, for deeply nested types, this narrowing doesn't happen automatically to maintain optimal TypeScript performance. The .narrow() method gives you explicit control over when to perform this more computationally expensive operation.

Example Usage

type Input = { user: { role: 'admin' | 'editor' | 'viewer' } };

declare const input: Input;

const result = match(input)
  .with({ user: { role: 'admin' } }, handleAdmin)
  .narrow() // Explicitly narrow remaining cases
  .with({ user: { role: 'editor' } }, handleEditor)
  .narrow() // Narrow again if needed
  .otherwise((remaining) => {
    // remaining.user.role is now precisely 'viewer'
    handleViewer(remaining);
  });

Key Benefits

  1. Precise Type Control: Get exactly the type information you need when you need it
  2. Type-checking Performance: Only pay the type-checking cost when necessary

Additional Improvements

  • Added a new release script to streamline our publishing process
  • Updated dependencies and package configurations

Full Changelog: v5.7.1...v5.8.0

PRs

  • feat: .narrow() method & typechecking perf improvement by @gvergnaud in #261

v5.7.1

18 May 13:48
8517f7f
Compare
Choose a tag to compare

Type inference bug fixes

This new release fixes the following bug in exhaustiveness checking when matching on optional properties:

type Input = { type?: 'one' } | { type: 'two' };

const f1 = (input: Input) =>
  match(input)
    .with({ type: 'one' }, () => {})
    .with({ type: 'two' }, () => {})
    .exhaustive(); // shouldn't type-check, but does 👎


const f2 = (input: Input) =>
  match(input)
    .with({ type: 'one' }, () => {})
    .with({ type: 'two' }, () => {})
    .with({ type: undefined }, () => {}) // <- the type key needs to be present.
    .exhaustive();  // shouldn't type-check, but does 👎

These two cases don't type check anymore. They fail with a NonExhaustiveError<{ type?: undefined; }>. To fix it, you should do:

type Input = { type?: 'one' } | { type: 'two' };

const f = (input: Input) =>
  match(input)
    .with({ type: 'one' }, () => {})
    .with({ type: 'two' }, () => {})
    .with({ type: P.optional(undefined) }, () => {}) // <- the type property may not be there
    .exhaustive(); // ✅

This is a purely type-level change, the runtime behavior is still the same.

What's Changed

Full Changelog: v5.7.0...v5.7.1

v5.7.0

28 Mar 20:35
60af216
Compare
Choose a tag to compare

New feature

Exhaustive callback

By default, .exhaustive() will throw an error if the input value wasn't handled by any .with(...) clause. This should only happen if your types are incorrect.

It is possible to pass your own handler function as a parameter to decide what should happen if an unexpected value has been received. You can for example throw your own custom error:

match(...)
  .with(...)
  .exhaustive((unexpected: unknown) => {
    throw MyCustomError(unexpected);
  })

Or log an error and return a default value:

match<string, number>(...)
  .with(P.string, (str) => str.length)
  .exhaustive((notAString: unknown) => {
    console.log(`received an unexpected value: ${notAString}`);
    return 0;
  })

Improved narrowing for isMatching

isMatching didn't have full feature parity with match in terms of type narrowing, but now does.

What's Changed

  • build(deps-dev): bump bun from 1.0.4 to 1.1.30 in /benchmarks by @dependabot in #303
  • isMatching: improve narrowing by @gvergnaud in #311
  • feat(exhaustive): Add support for passing a fallback function by @gvergnaud in #253

Full Changelog: v5.6.2...v5.7.0

v5.6.2

21 Jan 02:51
75e3967
Compare
Choose a tag to compare

What's Changed

Full Changelog: v5.6.1...v5.6.2

v5.6.1

19 Jan 15:18
c415e0c
Compare
Choose a tag to compare

What's Changed

  • fix(isMatching): Allow unknown properties in pattern by @gvergnaud in #305

Full Changelog: v5.6.0...v5.6.1

v5.6.0

15 Dec 17:40
Compare
Choose a tag to compare

This release contains two changes:

Typecheck pattern when using isMatching with 2 parameter.

It used to be possible to pass a pattern than could never match to isMatching. The new version checks that the provide pattern does match the value in second parameter:

type Pizza = { type: 'pizza'; topping: string };
type Sandwich = { type: 'sandwich'; condiments: string[] };
type Food = Pizza | Sandwich;

const fn = (food: Pizza | Sandwich) => {
    if (isMatching({ type: 'oops' }, food)) {
        //                  👆 used to type-check, now doesn't!
    }
}

Do not use P.infer as an inference point

When using P.infer<Pattern> to type a function argument, like in the following example:

const getWithDefault = <T extends P.Pattern>(
  input: unknown,
  pattern: T,
  defaultValue: P.infer<T> //  👈
): P.infer<T> =>
  isMatching(pattern, input) ? input : defaultValue

TypeScript could get confused and find type errors in the wrong spot:

const res = getWithDefault(null, { x: P.string }, 'oops') 
//                                     👆           👆 type error should be here
//                                 but it's here 😬

This new version fixes this problem.

What's Changed

Full Changelog: v5.5.0...v5.6.0

v5.5.0

14 Oct 03:05
Compare
Choose a tag to compare

What's Changed

New Contributors

Full Changelog: v5.4.0...v5.5.0

v5.4.0

25 Sep 22:24
Compare
Choose a tag to compare

The main thing — Faster type checking 🚀

This release brings a significant perf improvement to exhaustiveness checking, which led to a ~16% decrease in the time to type-check the full test suite of TS-Pattern:

Category Before After Evolution (%)
Instantiations 6,735,991 4,562,378 -32.33%
Memory used 732,233K 746,454K 1.95%
Assignability cache size 209,959 205,926 -1.92%
Identity cache size 28,093 28,250 0.56%
Check time 5.78s 4.83s -16.44%

What's Changed

  • build(deps-dev): bump braces from 3.0.2 to 3.0.3 by @dependabot in #273
  • build(deps-dev): bump webpack from 5.91.0 to 5.94.0 in /examples/gif-fetcher by @dependabot in #276
  • build(deps): bump serve-static and express in /examples/gif-fetcher by @dependabot in #283
  • perf: improve type checking performance of BuildMany by @gvergnaud in #286
  • Fixes type InvertPatternForExcludeInternal to work with readonly array by @changwoolab in #284

New Contributors

Full Changelog: v5.3.1...v5.4.0

v5.3.1

11 Aug 20:35
Compare
Choose a tag to compare

Pattern-matching on symbol keys

Symbols used to be ignored in object patterns. They are now taken into account:

const symbolA = Symbol('symbol-a');
const symbolB = Symbol('symbol-b');

const obj = { [symbolA]: { [symbolB]: 'foo' } };
    
if (isMatching({ [symbolA]: { [symbolB]: 'bar' } }, obj)) {
   //  👆 Used to return true, now returns false!
   
   //  Since TS-Pattern wasn't reading symbols, this pattern used to be equivalent
   //  to the `{}` pattern that matches any value except null and undefined.
}

.exhaustive now throws a custom error

People have expressed the need to differentiate runtime errors that .exhaustive() might throw when the input is of an unexpected type from other runtime errors that could have happened in the same match expression. It's now possible with err instanceof NonExhaustiveError:

import { match, P, NonExhaustiveError }  from 'ts-pattern';

const fn = (input: string | number) => {
  return match(input)
    .with(P.string, () => "string!")
    .with(P.number, () => "number!")
    .exhaustive()
}

try {
  fn(null as string) // 👈 💥 
} catch (e) {
  if (e instanceof NonExhaustiveError) {
    // The input was invalid
  } else {
    // something else happened
  }
}

What's Changed

  • build(deps-dev): bump braces from 3.0.2 to 3.0.3 in /examples/gif-fetcher by @dependabot in #262
  • feat: throw custom ExhaustiveError when no matched pattern by @adamhamlin in #270
  • Symbols as keys by @Ayc0 in #272

New Contributors

Full Changelog: v5.2.0...v5.3.1

v5.2.0

12 Jun 12:32
Compare
Choose a tag to compare

The main thing

new P.string.length(n) pattern

P.string.length(len) matches strings with exactly len characters.

const fn = (input: string) =>
  match(input)
    .with(P.string.length(2), () => '🎉')
    .otherwise(() => '❌');

console.log(fn('ok')); // logs '🎉'

What's Changed

New Contributors

Full Changelog: v5.1.2...v5.2.0