TS-Pattern
Thư viện Khớp Mẫu Toàn Diện cho TypeScript với sự suy luận kiểu thông minh.
import { match, P } from 'ts-pattern';
type Data =
| { type: 'text'; content: string }
| { type: 'img'; src: string };
type Result =
| { type: 'ok'; data: Data }
| { type: 'error'; error: Error };
const result: Result = ...;
const html = match(result)
.with({ type: 'error' }, () => `<p>Oups! An error occured</p>`)
.with({ type: 'ok', data: { type: 'text' } }, (res) => `<p>${res.data.content}</p>`)
.with({ type: 'ok', data: { type: 'img', src: P.select() } }, (src) => `<img src=${src} />`)
.exhaustive();
Về
Viết điều kiện tốt hơn và an toàn hơn. Khớp mẫu cho phép bạn diễn đạt các điều kiện phức tạp trong một biểu thức duy nhất và gọn gàng. Mã của bạn trở nên ngắn hơn và dễ đọc hơn. Kiểm tra toàn diện đảm bảo bạn không quên bất kỳ trường hợp nào.
Hoạt hình bởi @nicoespeon
Tính năng
- Khớp mẫu trên bất kỳ cấu trúc dữ liệu nào : Đối tượng lồng nhau, Mảng, Bộ đôi, Tập hợp, Bản đồ và tất cả các loại nguyên thủy.
- An toàn kiểu , với sự suy luận kiểu hữu ích.
- Hỗ trợ kiểm tra toàn diện , đảm bảo bạn đang khớp với mọi trường hợp có thể với
.toàn_diện()
. - Sử dụng các mẫu để xác minh hình dạng của dữ liệu của bạn với
isMatching
. - API diễn đạt , với catch-all và loại cụ thể của kiểu ký tự đại diện :
P._
,P.string
,P.number
, vv. - Hỗ trợ predicate, union, intersection và các mẫu loại trừ cho các trường hợp không đơn giản.
- Hỗ trợ lựa chọn thuộc tính, thông qua hàm
P.select(tên?)
. - Kích thước gói nhỏ (only ~2kB).
Khớp Mẫu là Gì?
Pattern Matching là một kỹ thuật nhánh mã nguồn đến từ các ngôn ngữ lập trình hàm mạnh mẽ và thường ít dài hơn so với các lựa chọn thực hiện mệnh đề (if/else/switch statements), đặc biệt là đối với các điều kiện phức tạp.
Khớp Mẫu được thực hiện trong Haskell, Rust, Swift, Elixir và nhiều ngôn ngữ khác. Có a tc39 proposal để thêm Khớp Mẫu vào EcmaScript, nhưng nó vẫn ở giai đoạn 1 và không có khả năng đến trước vài năm. May mắn thay, khớp mẫu có thể được thực hiện trong userland. ts-pattern
Cung cấp một triển khai khớp mẫu an toàn kiểu bạn có thể bắt đầu sử dụng ngay hôm nay.
Đọc bài viết giới thiệu: Bringing Pattern Matching to TypeScript 🎨 Introducing TS-Pattern
Cài Đặt
Thông qua npm
npm install ts-pattern
Thông qua yarn
yarn add ts-pattern
Tương thích với các phiên bản TypeScript khác nhau
TS-Pattern giả định rằng Strict Mode đã được kích hoạt trong tệp tsconfig.json
của bạn.
ts-pattern | TypeScript v5+ | TypeScript v4.5+ | TypeScript v4.2+ |
---|---|---|---|
v5.x (Tài liệu) (Migration Guide) | ✅ | ❌ | ❌ |
v4.x (Docs) (Migration Guide) | ✅ | ✅ | ❌ |
v3.x (Docs) | ✅ | ✅ | ✅ |
- ✅ Hỗ trợ đầy đủ
- ❌ Không được hỗ trợ
Ví dụ Code Sandbox
- Basic Demo
- React gif fetcher app Demo
- React.useReducer Demo
- Handling untyped API response Demo
- P.when Guard Demo
- P.not Pattern Demo
- P.select Pattern Demo
- P.union Pattern Demo
Bắt Đầu
Ví dụ, hãy tạo một trình giảm trạng thái cho một ứng dụng frontend có chức năng truy xuất dữ liệu nào đó.
Ví dụ: một trình giảm trạng thái với ts-pattern
Ứng dụng của chúng tôi có thể ở bốn trạng thái khác nhau: idle
, loading
, success
và error
. Tùy thuộc vào trạng thái chúng ta đang ở, một số sự kiện có thể xảy ra. Dưới đây là tất cả các loại sự kiện có thể xảy ra trong ứng dụng của chúng tôi: fetch
, success
, error
và cancel
.
Tôi sử dụng từ sự kiện
nhưng bạn có thể thay thế nó bằng từ hành động
nếu bạn quen thuộc với thuật ngữ của Redux.
type State =
| { status: 'idle' }
| { status: 'loading'; startTime: number }
| { status: 'success'; data: string }
| { status: 'error'; error: Error };
type Event =
| { type: 'fetch' }
| { type: 'success'; data: string }
| { type: 'error'; error: Error }
| { type: 'cancel' };
Mặc dù ứng dụng của chúng tôi có thể xử lý 4 sự kiện, chỉ một phần của những sự kiện này hợp lý cho mỗi trạng thái cụ thể. Ví dụ, chúng ta chỉ có thể hủy
một yêu cầu nếu hiện tại chúng ta đang ở trạng thái loading
. Để tránh các thay đổi trạng thái không mong muốn có thể dẫn đến lỗi, chúng ta muốn hàm giảm trạng thái của chúng ta nhánh trên cả trạng thái và sự kiện , và trả về một trạng thái mới.
Đây là một trường hợp mà khớp
thực sự tỏa sáng. Thay vì viết các câu lệnh chuyển đổi lồng nhau, chúng ta có thể sử dụng khớp mẫu để đồng thời kiểm tra trạng thái và đối tượng sự kiện:
import { match, P } from 'ts-pattern';
const reducer = (state: State, event: Event) =>
match([state, event])
.returnType<State>()
.with(
[{ status: 'loading' }, { type: 'success' }],
([_, event]) => ({ status: 'success', data: event.data })
)
.with(
[{ status: 'loading' }, { type: 'error', error: P.select() }],
(error) => ({ status: 'error', error })
)
.with(
[{ status: P.not('loading') }, { type: 'fetch' }],
() => ({ status: 'loading', startTime: Date.now() })
)
.with(
[
{
status: 'loading',
startTime: P.when((t) => t + 2000 < Date.now()),
},
{ type: 'cancel' },
],
() => ({ status: 'idle' })
)
.with(P._, () => state)
.exhaustive();
Có rất nhiều điều xảy ra, vì vậy hãy đi qua mã này từng chút một:
khớp(giá_trị)
khớp
nhận một giá trị và trả về một builder mà bạn có thể thêm các trường hợp khớp mẫu của bạn vào đó.
match([state, event])
Cũng có thể chỉ định rõ kiểu đầu vào và đầu ra bằng cách sử dụng khớp<Đầu_vào, Đầu_ra>(...)
, nhưng điều này thường không cần thiết, vì TS-Pattern có khả năng suy luận chúng.
.kiểu_đầu_ra()
.kiểu_đầu_ra
là một phương thức tùy chọn mà bạn có thể gọi nếu bạn muốn buộc tất cả các nhánh mã sau đó trả về một giá trị của một kiểu cụ thể. Nó nhận một tham số kiểu duy nhất, được cung cấp giữa <Góc_đặc_biệt>
.
.returnType<State>()
Ở đây, chúng tôi sử dụng phương thức này để đảm bảo tất cả các nhánh trả về một đối tượng Trạng_thái
hợp lệ.
.với(mẫu, bộ_xử_lý)
Sau đó, chúng ta thêm một mệnh đề với
đầu tiên:
.with(
[{ status: 'loading' }, { type: 'success' }],
([state, event]) => ({
// `state` is inferred as { status: 'loading' }
// `event` is inferred as { type: 'success', data: string }
status: 'success',
data: event.data,
})
)
Đối số đầu tiên là mẫu : hình dạng của giá trị bạn mong đợi cho nhánh này.
Đối số thứ hai là hàm xử lý : mệnh đề mã sẽ được gọi nếu giá trị đầu vào khớp với mẫu.
Hàm xử lý lấy giá trị đầu vào làm tham số đầu tiên với kiểu của nó được thu gọn thành những gì mẫu khớp.
P.chọn(tên?)
Trong mệnh đề với
thứ hai, chúng ta sử dụng hàm P.chọn
:
.with(
[
{ status: 'loading' },
{ type: 'error', error: P.select() }
],
(error) => ({ status: 'error', error })
)
P.chọn()
cho phép bạn trích xuất một phần của giá trị đầu vào của bạn và chèn nó vào bộ xử lý của bạn. Điều này rất hữu ích khi khớp mẫu trên cấu trúc dữ liệu sâu vì nó loại bỏ sự phiền phức của việc giải cấu trúc đầu vào trong bộ xử lý của bạn.
Vì chúng ta không truyền tên nào cho P.chọn()
, nó sẽ chèn thuộc tính event.error
làm tham số đầu tiên vào hàm xử lý. Lưu ý rằng bạn vẫn có thể truy cập giá trị đầu vào đầy đủ với kiểu bị thu hẹp bởi mẫu của bạn như tham số thứ hai của hàm xử lý:
.with(
[
{ status: 'loading' },
{ type: 'error', error: P.select() }
],
(error, stateAndEvent) => {
// error: Error
// stateAndEvent: [{ status: 'loading' }, { type: 'error', error: Error }]
}
)
Trong một mẫu, chúng ta chỉ có thể có một lựa chọn ẩn. Nếu bạn cần chọn nhiều thuộc tính trên cấu trúc dữ liệu đầu vào của bạn, bạn sẽ cần đặt cho chúng tên :
.with(
[
{ status: 'success', data: P.select('prevData') },
{ type: 'error', error: P.select('err') }
],
({ prevData, err }) => {
// Do something with (prevData: string) and (err: Error).
}
)
Mỗi lựa chọn có tên sẽ được chèn vào bên trong một đối tượng lựa chọn
, được truyền làm tham số đầu tiên vào hàm xử lý. Tên có thể là bất kỳ chuỗi nào.
P.không(mẫu)
Nếu bạn cần khớp với mọi thứ ngoại trừ một giá trị cụ thể, bạn có thể sử dụng một mẫu P.không(<mẫu>)
. Đó là một hàm nhận một mẫu và trả về phản nghịch của nó:
.with(
[{ status: P.not('loading') }, { type: 'fetch' }],
() => ({ status: 'loading' })
)
P.when()
và các hàm bảo vệ
Đôi khi, chúng ta cần đảm bảo giá trị đầu vào của chúng ta tuân theo một điều kiện không thể biểu thị bằng một mẫu. Ví dụ, hãy tưởng tượng bạn cần kiểm tra rằng một số là số dương. Trong những trường hợp này, chúng ta có thể sử dụng các hàm bảo vệ : các hàm nhận một giá trị và trả về một boolean
.
Với TS-Pattern, có hai cách để sử dụng một hàm bảo vệ:
- sử dụng
P.when(<hàm bảo vệ>)
bên trong một trong các mẫu của bạn - truyền nó như tham số thứ hai cho
.với(...)
sử dụng P.when(predicate)
.with(
[
{
status: 'loading',
startTime: P.when((t) => t + 2000 < Date.now()),
},
{ type: 'cancel' },
],
() => ({ status: 'idle' })
)
Truyền một hàm bảo vệ vào .với(...)
.với
tùy chọn chấp nhận một hàm bảo vệ như tham số thứ hai, giữa mẫu
và gọi lại handler
:
.with(
[{ status: 'loading' }, { type: 'cancel' }],
([state, event]) => state.startTime + 2000 < Date.now(),
() => ({ status: 'idle' })
)
Mẫu này chỉ khớp nếu hàm bảo vệ trả về true
.
ký tự đại diện P._
P._
sẽ phù hợp với bất kỳ giá trị nào. Bạn có thể sử dụng nó ở cấp độ cao nhất hoặc bên trong một mẫu khác.
.with(P._, () => state)
// You could also use it inside another pattern:
.with([P._, P._], () => state)
// at any level:
.with([P._, { type: P._ }], () => state)
.exhaustive(), .otherwise() và .run()
.exhaustive();
.exhaustive()
thực thi biểu thức kiểm tra mẫu và trả về kết quả. Nó cũng bật kiểm tra đầy đủ , đảm bảo chúng ta không bỏ sót bất kỳ trường hợp nào trong giá trị đầu vào của chúng ta. Điều này giúp đảm bảo tính an toàn kiểu dữ liệu bổ sung bởi việc quên một trường hợp là một sai lầm dễ xảy ra, đặc biệt trong một mã nguồn đang tiến hóa.
Lưu ý rằng kiểm tra đầy đủ mẫu là tùy chọn. Nó đi kèm với sự đánh đổi của thời gian biên dịch lâu hơn một chút vì trình kiểm tra kiểu phải làm nhiều công việc hơn.
Hoặc bạn có thể sử dụng .otherwise()
, nó nhận một hàm xử lý trả về một giá trị mặc định. .otherwise(handler)
tương đương với .with(P._, handler).exhaustive()
.
.otherwise(() => state);
Khớp nhiều mẫu
Như bạn có thể biết, các câu lệnh switch
cho phép xử lý nhiều trường hợp với cùng một khối mã:
switch (type) {
case 'text':
case 'span':
case 'p':
return 'text';
case 'btn':
case 'button':
return 'button';
}
Tương tự, ts-pattern cho phép bạn truyền nhiều mẫu vào .with()
và nếu một trong những mẫu này phù hợp với đầu vào của bạn, hàm xử lý sẽ được gọi:
const sanitize = (name: string) =>
match(name)
.with('text', 'span', 'p', () => 'text')
.with('btn', 'button', () => 'button')
.otherwise(() => name);
sanitize('span'); // 'text'
sanitize('p'); // 'text'
sanitize('button'); // 'button'
Như bạn có thể mong đợi, điều này cũng hoạt động với các mẫu phức tạp hơn chuỗi và kiểm tra đầy đủ mẫu cũng hoạt động.
Tài liệu tham khảo API
match
match(value);
Tạo một đối tượng Match
mà bạn có thể gọi sau đó .with
, .when
, .otherwise
và .run
.
Chữ ký
function match<TInput, TOutput>(input: TInput): Match<TInput, TOutput>;
Đối số
input
- Yêu cầu
- giá trị đầu vào mà các mẫu của bạn sẽ được kiểm tra.
.with
match(...)
.with(pattern, [...patterns], handler)
Chữ ký
function with(
pattern: Pattern<TInput>,
handler: (selections: Selections<TInput>, value: TInput) => TOutput
): Match<TInput, TOutput>;
// Overload for multiple patterns
function with(
pattern1: Pattern<TInput>,
...patterns: Pattern<TInput>[],
// no selection object is provided when using multiple patterns
handler: (value: TInput) => TOutput
): Match<TInput, TOutput>;
// Overload for guard functions
function with(
pattern: Pattern<TInput>,
when: (value: TInput) => unknown,
handler: (
selection: Selection<TInput>,
value: TInput
) => TOutput
): Match<TInput, TOutput>;
Đối số
pattern: Pattern<TInput>
- Yêu cầu
- Mẫu mà đầu vào của bạn phải phù hợp để gọi hàm xử lý.
- Xem tất cả các mẫu hợp lệ dưới đây
- Nếu bạn cung cấp nhiều mẫu trước khi cung cấp
handler
, điều khoảnwith
sẽ phù hợp nếu một trong các mẫu phù hợp. when: (value: TInput) => unknown
- Tùy chọn
- Điều kiện bổ sung mà đầu vào phải thỏa mãn để gọi hàm xử lý.
- Đầu vào sẽ phù hợp nếu hàm bảo vệ của bạn trả về một giá trị truthy.
TInput
có thể được thu hẹp thành một kiểu chính xác hơn bằng cách sử dụngpattern
.handler: (value: TInput, selections: Selections<TInput>) => TOutput
- Yêu cầu
- Hàm được gọi khi điều kiện khớp được thỏa mãn.
- Tất cả các hàm xử lý trong một trường hợp
match
duy nhất phải trả về các giá trị cùng loại,TOutput
. TInput
có thể được thu hẹp thành một kiểu chính xác hơn bằng cách sử dụngpattern
.selections
là một đối tượng các thuộc tính được lựa chọn từ đầu vào bằng cách sử dụng hàmselect
.
.when
match(...)
.when(predicate, handler)
Chữ ký
function when(
predicate: (value: TInput) => unknown,
handler: (value: TInput) => TOutput
): Match<TInput, TOutput>;
Đối số
predicate: (value: TInput) => unknown
- Yêu cầu
- Điều kiện mà đầu vào phải thỏa mãn để gọi hàm xử lý.
handler: (value: TInput) => TOutput
- Yêu cầu
- Hàm được gọi khi điều kiện điều kiện được thỏa mãn.
- Tất cả các hàm xử lý trong một trường hợp
match
duy nhất phải trả về các giá trị cùng loại,TOutput
.
.returnType
match(...)
.returnType<string>()
.with(..., () => "has to be a string")
.with(..., () => "Oops".length)
// ~~~~~~~~~~~~~ ❌ `number` isn't a string!
Phương thức .returnType<SomeType>()
cho phép bạn kiểm soát kiểu trả về của tất cả các nhánh code của bạn. Nó chấp nhận một tham số kiểu duy nhất sẽ được sử dụng làm kiểu trả về của biểu thức match
của bạn. Tất cả các nhánh code phải trả về các giá trị có thể gán được cho kiểu này.
Chữ ký
function returnType<TOutputOverride>(): Match<TInput, TOutputOverride>;
Tham số kiểu
TOutputOverride
- Kiểu mà biểu thức
match
của bạn sẽ trả về. Tất cả các nhánh phải trả về các giá trị có thể gán được cho nó.
.exhaustive
match(...)
.with(...)
.exhaustive()
Chạy biểu thức khớp mẫu và trả về kết quả của nó. Nó cũng cho phép kiểm tra tính toàn diện, đảm bảo tại thời gian biên dịch rằng chúng ta đã xử lý tất cả các trường hợp có thể.
Chữ ký
function exhaustive(): TOutput;
Ví dụ
type Permission = 'editor' | 'viewer';
type Plan = 'basic' | 'pro';
const fn = (org: Plan, user: Permission) =>
match([org, user])
.with(['basic', 'viewer'], () => {})
.with(['basic', 'editor'], () => {})
.with(['pro', 'viewer'], () => {})
// Fails with `NonExhaustiveError<['pro', 'editor']>`
// because the `['pro', 'editor']` case isn't handled.
.exhaustive();
const fn2 = (org: Plan, user: Permission) =>
match([org, user])
.with(['basic', 'viewer'], () => {})
.with(['basic', 'editor'], () => {})
.with(['pro', 'viewer'], () => {})
.with(['pro', 'editor'], () => {})
.exhaustive(); // Works!
.otherwise
match(...)
.with(...)
.otherwise(defaultHandler)
Chạy biểu thức khớp mẫu với một xử lý mặc định sẽ được gọi nếu không có mệnh đề .with()
nào phù hợp với giá trị đầu vào, và trả về kết quả.
Chữ ký
function otherwise(defaultHandler: (value: TInput) => TOutput): TOutput;
Đối số
defaultHandler: (value: TInput) => TOutput
- Yêu cầu
- Hàm được gọi nếu không có mẫu nào phù hợp với giá trị đầu vào.
- Hãy xem nó như trường hợp
default:
trong câu lệnhswitch
. - Tất cả các xử lý trên một trường hợp
match
phải trả về các giá trị cùng kiểu,TOutput
.
.run
match(...)
.with(...)
.run()
Trả về kết quả của biểu thức khớp mẫu hoặc ném ngoại lệ nếu không có mẫu nào phù hợp với giá trị đầu vào. .run()
tương tự như .exhaustive()
, nhưng không an toàn vì tính toàn diện không được kiểm tra tại thời gian biên dịch, vì vậy bạn không có đảm bảo rằng tất cả các trường hợp đều được bao phủ. Sử dụng có nguy cơ riêng của bạn.
Chữ ký
function run(): TOutput;
isMatching
if (isMatching(pattern, value)) {
...
}
isMatching
là một hàm kiểm tra kiểu dữ liệu kiểm tra xem một mẫu có phù hợp với giá trị đã cho hay không. Nó được thực hiện dưới dạng hàm curried, có nghĩa là nó có thể được sử dụng theo hai cách.
Với một đối số duy nhất:
import { isMatching, P } from 'ts-pattern';
const isBlogPost = isMatching({
type: 'blogpost',
title: P.string,
description: P.string,
});
if (isBlogPost(value)) {
// value: { type: 'blogpost', title: string, description: string }
}
Với hai đối số:
const blogPostPattern = {
type: 'blogpost',
title: P.string,
description: P.string,
} as const;
if (isMatching(blogPostPattern, value)) {
// value: { type: 'blogpost', title: string, description: string }
}
Chữ ký
export function isMatching<p extends Pattern<any>>(
pattern: p
): (value: any) => value is InvertPattern<p>;
export function isMatching<p extends Pattern<any>>(
pattern: p,
value: any
): value is InvertPattern<p>;
Đối số
pattern: Pattern<any>
- Bắt buộc
- Mẫu mà giá trị nên phù hợp.
value?: any
- Tùy chọn
- Nếu một giá trị được cung cấp như là đối số thứ hai,
isMatching
sẽ trả về một giá trị boolean cho chúng ta biết liệu mẫu có phù hợp với giá trị hay không. - Nếu chúng ta chỉ cung cấp mẫu cho hàm,
isMatching
sẽ trả về một hàm kiểm tra kiểu dữ liệu khác type guard function lấy một giá trị và trả về một giá trị boolean cho chúng ta biết liệu mẫu có phù hợp với giá trị hay không.
Mẫu
Một mẫu là mô tả về hình dạng dự kiến của giá trị đầu vào của bạn.
Mẫu có thể là giá trị JavaScript thông thường ("chuỗi nào đó"
, 10
, true
, …), cấu trúc dữ liệu (đối tượng, mảng, …), biểu thức đại diện (P._
, P.string
, P.number
, …), hoặc các hàm kiểm tra đặc biệt (P.not
, P.when
, P.select
, …).
Tất cả các biểu thức đại diện và các hàm kiểm tra có thể được nhập vào dưới dạng Pattern
hoặc P
từ module ts-pattern
.
import { match, Pattern } from 'ts-pattern';
const toString = (value: unknown): string =>
match(value)
.with(Pattern.string, (str) => str)
.with(Pattern.number, (num) => num.toFixed(2))
.with(Pattern.boolean, (bool) => `${bool}`)
.otherwise(() => 'Unknown');
Hoặc
import { match, P } from 'ts-pattern';
const toString = (value: unknown): string =>
match(value)
.with(P.string, (str) => str)
.with(P.number, (num) => num.toFixed(2))
.with(P.boolean, (bool) => `${bool}`)
.otherwise(() => 'Unknown');
Nếu đầu vào của bạn không có kiểu dữ liệu (nếu nó là any
hoặc unknown
), bạn có thể sử dụng bất kỳ biểu thức đại diện nào. Bộ xử lý của bạn sẽ suy ra kiểu dữ liệu đầu vào từ hình dạng của mẫu của bạn.
Thể hiện
Thể hiện là các giá trị nguyên thủy của JavaScript, như số
, chuỗi
, boolean
, bigints
, symbols
, null
, undefined
, hoặc NaN
.
import { match } from 'ts-pattern';
const input: unknown = 2;
const output = match(input)
.with(2, () => 'number: two')
.with(true, () => 'boolean: true')
.with('hello', () => 'string: hello')
.with(undefined, () => 'undefined')
.with(null, () => 'null')
.with(NaN, () => 'number: NaN')
.with(20n, () => 'bigint: 20n')
.otherwise(() => 'something else');
console.log(output);
// => 'number: two'
Đối tượng
Mẫu có thể là đối tượng chứa các mẫu con. Một mẫu đối tượng sẽ phù hợp nếu và chỉ nếu giá trị đầu vào là một đối tượng, chứa tất cả các thuộc tính mà mẫu xác định và mỗi thuộc tính phù hợp với mẫu con tương ứng.
import { match } from 'ts-pattern';
type Input =
| { type: 'user'; name: string }
| { type: 'image'; src: string }
| { type: 'video'; seconds: number };
let input: Input = { type: 'user', name: 'Gabriel' };
const output = match(input)
.with({ type: 'image' }, () => 'image')
.with({ type: 'video', seconds: 10 }, () => 'video of 10 seconds.')
.with({ type: 'user' }, ({ name }) => `user of name: ${name}`)
.otherwise(() => 'something else');
console.log(output);
// => 'user of name: Gabriel'
Tuples (mảng)
Trong TypeScript, Tuples là các mảng với một số lượng phần tử cố định có thể có các kiểu khác nhau. Bạn có thể sử dụng mẫu tuple để khớp mảng. Một mẫu tuple sẽ phù hợp nếu giá trị đầu vào là một mảng có cùng độ dài, và mỗi phần tử phù hợp với mẫu con tương ứng.
import { match, P } from 'ts-pattern';
type Input =
| [number, '+', number]
| [number, '-', number]
| [number, '*', number]
| ['-', number];
const input: Input = [3, '*', 4];
const output = match(input)
.with([P._, '+', P._], ([x, , y]) => x + y)
.with([P._, '-', P._], ([x, , y]) => x - y)
.with([P._, '*', P._], ([x, , y]) => x * y)
.with(['-', P._], ([, x]) => -x)
.otherwise(() => NaN);
console.log(output);
// => 12
Biểu thức đại diện (wildcards)
Biểu thức đại diện P._
Mẫu P._
sẽ phù hợp với bất kỳ giá trị nào. Bạn cũng có thể sử dụng P.any
, đó là một tên gọi khác cho P._
.
import { match, P } from 'ts-pattern';
const input = 'hello';
const output = match(input)
.with(P._, () => 'It will always match')
// OR
.with(P.any, () => 'It will always match')
.otherwise(() => 'This string will never be used');
console.log(output);
// => 'It will always match'
Biểu thức đại diện P.string
Mẫu P.string
sẽ phù hợp với bất kỳ giá trị thuộc kiểu string
.
import { match, P } from 'ts-pattern';
const input = 'hello';
const output = match(input)
.with('bonjour', () => 'Won‘t match')
.with(P.string, () => 'it is a string!')
.exhaustive();
console.log(output);
// => 'it is a string!'
Biểu thức đại diện P.number
Mẫu P.number
sẽ phù hợp với bất kỳ giá trị thuộc kiểu number
.
import { match, P } from 'ts-pattern';
const input = 2;
const output = match<number | string>(input)
.with(P.string, () => 'it is a string!')
.with(P.number, () => 'it is a number!')
.exhaustive();
console.log(output);
// => 'it is a number!'
Biểu thức đại diện P.boolean
Mẫu P.boolean
sẽ phù hợp với bất kỳ giá trị thuộc kiểu boolean
.
import { match, P } from 'ts-pattern';
const input = true;
const output = match<number | string | boolean>(input)
.with(P.string, () => 'it is a string!')
.with(P.number, () => 'it is a number!')
.with(P.boolean, () => 'it is a boolean!')
.exhaustive();
console.log(output);
// => 'it is a boolean!'
Biểu thức đại diện P.nullish
Mẫu P.nullish
sẽ phù hợp với bất kỳ giá trị thuộc kiểu null
hoặc undefined
.
Mặc dù null
và undefined
có thể được sử dụng như các mẫu cụ thể, đôi khi chúng xuất hiện trong một liên hợp cùng nhau (ví dụ: null | undefined | string
) và bạn có thể muốn coi chúng như tương đương bằng cách sử dụng P.nullish
.
import { match, P } from 'ts-pattern';
const input = null;
const output = match<number | null | undefined>(input)
.with(P.number, () => 'it is a number!')
.with(P.nullish, () => 'it is either null or undefined!')
.with(null, () => 'it is null!')
.with(undefined, () => 'it is undefined!')
.exhaustive();
console.log(output);
// => 'it is either null or undefined!'
Biểu thức đại diện P.bigint
Mẫu P.bigint
sẽ phù hợp với bất kỳ giá trị thuộc kiểu bigint
.
import { match, P } from 'ts-pattern';
const input = 20000000n;
const output = match<bigint | null>(input)
.with(P.bigint, () => 'it is a bigint!')
.otherwise(() => '?');
console.log(output);
// => 'it is a bigint!'
Biểu thức đại diện P.symbol
Mẫu P.symbol
sẽ phù hợp với bất kỳ giá trị thuộc kiểu symbol
.
import { match, P } from 'ts-pattern';
const input = Symbol('some symbol');
const output = match<symbol | null>(input)
.with(P.symbol, () => 'it is a symbol!')
.otherwise(() => '?');
console.log(output);
// => 'it is a symbol!'
Mẫu P.array
Để so khớp trên các mảng có kích thước không biết trước, bạn có thể sử dụng P.array(subpattern)
. Nó lấy một mẫu con và sẽ so khớp nếu tất cả các phần tử trong mảng đầu vào phù hợp với mẫu con này.
import { match, P } from 'ts-pattern';
type Input = { title: string; content: string }[];
let input: Input = [
{ title: 'Hello world!', content: 'This is a very interesting content' },
{ title: 'Bonjour!', content: 'This is a very interesting content too' },
];
const output = match(input)
.with(
P.array({ title: P.string, content: P.string }),
(posts) => 'a list of posts!'
)
.otherwise(() => 'something else');
console.log(output);
// => 'a list of posts!'
So khớp các tuple đa biến bằng cách sử dụng P.array
Trong TypeScript, Variadic Tuple Types là các loại mảng được tạo ra bằng toán tử ...
, như [string, ...string[]]
, [number, ...boolean[], string]
vv. Bạn có thể so khớp với các loại tuple đa biến bằng cách sử dụng các mảng chứa ...P.array(subpattern)
:
import { match, P } from 'ts-pattern';
type Input = (number | string)[];
declare const input: Input;
const output = match(input)
// P.array's parameter is optional
.with([P.string, ...P.array()], (input) => input) // input: [string, ...(number | string)[]]
.with(['print', ...P.array(P.string)], (input) => input) // input: ['print', ...string[]]
// you can put patterns on either side of `...P.array()`:
.with([...P.array(P.string), 'end'], (input) => input) // input: [...string[], 'end']
.with(['start', ...P.array(P.string), 'end'], (input) => input) // input: ['start', ...string[], 'end']
.otherwise((input) => input);
Mẫu P.set
Để so khớp với một Set, bạn có thể sử dụng P.set(subpattern)
. Nó lấy một mẫu con và sẽ so khớp nếu tất cả các phần tử bên trong Set phù hợp với mẫu con này.
import { match, P } from 'ts-pattern';
type Input = Set<string | number>;
const input: Input = new Set([1, 2, 3]);
const output = match(input)
.with(P.set(1), (set) => `Set contains only 1`)
.with(P.set(P.string), (set) => `Set contains only strings`)
.with(P.set(P.number), (set) => `Set contains only numbers`)
.otherwise(() => '');
console.log(output);
// => "Set contains only numbers"
Mẫu P.map
Để so khớp với một Map, bạn có thể sử dụng P.map(keyPattern, valuePattern)
. Nó lấy một mẫu con để so khớp với khóa, một mẫu con để so khớp với giá trị và sẽ so khớp nếu tất cả các phần tử bên trong bản đồ này phù hợp với hai mẫu con này.
import { match, P } from 'ts-pattern';
type Input = Map<string, string | number>;
const input: Input = new Map([
['a', 1],
['b', 2],
['c', 3],
]);
const output = match(input)
.with(P.map(P.string, P.number), (map) => `map's type is Map<string, number>`)
.with(P.map(P.string, P.string), (map) => `map's type is Map<string, string>`)
.with(
P.map(P.union('a', 'c'), P.number),
(map) => `map's type is Map<'a' | 'c', number>`
)
.otherwise(() => '');
console.log(output);
// => "map's type is Map<string, number>"
Mẫu P.when
P.when
cho phép bạn xác định logic của riêng mình để kiểm tra xem mẫu có nên khớp hay không. Nếu hàm predicate
được đưa cho when
trả về một giá trị đúng, thì mẫu sẽ khớp với đầu vào này.
Lưu ý rằng bạn có thể thu hẹp loại của đầu vào của mình bằng cách cung cấp một Type Guard function cho P.when
.
import { match, P } from 'ts-pattern';
type Input = { score: number };
const output = match({ score: 10 })
.with(
{
score: P.when((score): score is 5 => score === 5),
},
(input) => '😐' // input is inferred as { score: 5 }
)
.with({ score: P.when((score) => score < 5) }, () => '😞')
.with({ score: P.when((score) => score > 5) }, () => '🙂')
.run();
console.log(output);
// => '🙂'
Mẫu P.not
P.not
cho phép bạn khớp với mọi thứ ngoại trừ một giá trị cụ thể. Đó là một hàm lấy một mẫu và trả về mẫu ngược lại.
import { match, P } from 'ts-pattern';
type Input = boolean | number;
const toNumber = (input: Input) =>
match(input)
.with(P.not(P.boolean), (n) => n) // n: number
.with(true, () => 1)
.with(false, () => 0)
.run();
console.log(toNumber(2));
// => 2
console.log(toNumber(true));
// => 1
Mẫu P.select
P.select
cho phép bạn chọn một phần của cấu trúc dữ liệu đầu vào của bạn và chèn nó vào hàm xử lý của bạn.
Điều này đặc biệt hữu ích khi so khớp mẫu trên cấu trúc dữ liệu sâu để tránh sự phiền toái của việc giải cấu trúc trong hàm xử lý.
Lựa chọn có thể được đặt tên (với P.select('someName')
) hoặc ẩn danh (với P.select()
).
Bạn chỉ có thể có một lựa chọn ẩn danh cho mẫu, và giá trị được chọn sẽ được chèn trực tiếp vào hàm xử lý của bạn như đối số đầu tiên:
import { match, P } from 'ts-pattern';
type Input =
| { type: 'post'; user: { name: string } }
| { ... };
const input: Input = { type: 'post', user: { name: 'Gabriel' } }
const output = match(input)
.with(
{ type: 'post', user: { name: P.select() } },
username => username // username: string
)
.otherwise(() => 'anonymous');
console.log(output);
// => 'Gabriel'
Nếu bạn cần chọn nhiều thứ bên trong cấu trúc dữ liệu đầu vào của bạn, bạn có thể đặt tên cho các lựa chọn của bạn bằng cách đưa một chuỗi cho P.select(<tên>)
. Mỗi lựa chọn sẽ được truyền làm đối số đầu tiên vào hàm xử lý của bạn dưới dạng một đối tượng.
import { match, P } from 'ts-pattern';
type Input =
| { type: 'post'; user: { name: string }, content: string }
| { ... };
const input: Input = { type: 'post', user: { name: 'Gabriel' }, content: 'Hello!' }
const output = match(input)
.with(
{ type: 'post', user: { name: P.select('name') }, content: P.select('body') },
({ name, body }) => `${name} wrote "${body}"`
)
.otherwise(() => '');
console.log(output);
// => 'Gabriel wrote "Hello!"'
Bạn cũng có thể truyền một mẫu con cho P.select
nếu bạn muốn nó chỉ chọn các giá trị phù hợp với mẫu con này:
type User = { age: number; name: string };
type Post = { body: string };
type Input = { author: User; content: Post };
declare const input: Input;
const output = match(input)
.with(
{
author: P.select({ age: P.number.gt(18) }),
},
(author) => author // author: User
)
.with(
{
author: P.select('author', { age: P.number.gt(18) }),
content: P.select(),
},
({ author, content }) => author // author: User, content: Post
)
.otherwise(() => 'anonymous');
Mẫu P.optional
P.optional(subpattern)
cho phép bạn đánh dấu một khóa trong mẫu đối tượng là tùy chọn, nhưng nếu nó được định nghĩa thì nó phải phù hợp với một mẫu con cụ thể.
import { match, P } from 'ts-pattern';
type Input = { key?: string | number };
const output = match(input)
.with({ key: P.optional(P.string) }, (a) => {
return a.key; // string | undefined
})
.with({ key: P.optional(P.number) }, (a) => {
return a.key; // number | undefined
})
.exhaustive();
Mẫu P.instanceOf
Hàm P.instanceOf
cho phép bạn xây dựng một mẫu để kiểm tra xem một giá trị có phải là một thể hiện của một lớp không:
import { match, P } from 'ts-pattern';
class A {
a = 'a';
}
class B {
b = 'b';
}
type Input = { value: A | B };
const input: Input = { value: new A() };
const output = match(input)
.with({ value: P.instanceOf(A) }, (a) => {
return 'instance of A!';
})
.with({ value: P.instanceOf(B) }, (b) => {
return 'instance of B!';
})
.exhaustive();
console.log(output);
// => 'instance of A!'
Mẫu P.union
P.union(...subpatterns)
cho phép bạn kiểm tra nhiều mẫu và sẽ khớp nếu một trong những mẫu này khớp. Điều này đặc biệt hữu ích khi bạn muốn xử lý một số trường hợp của một loại hợp nhất trong cùng một nhánh mã:
import { match, P } from 'ts-pattern';
type Input =
| { type: 'user'; name: string }
| { type: 'org'; name: string }
| { type: 'text'; content: string }
| { type: 'img'; src: string };
declare const input: Input;
const output = match(input)
.with({ type: P.union('user', 'org') }, (userOrOrg) => {
// userOrOrg: User | Org
return userOrOrg.name;
})
.otherwise(() => '');
Mẫu P.intersection
P.intersection(...subpatterns)
cho phép bạn đảm bảo rằng đầu vào phù hợp với tất cả các mẫu con được truyền như tham số.
class A {
constructor(public foo: 'bar' | 'baz') {}
}
class B {
constructor(public str: string) {}
}
type Input = { prop: A | B };
declare const input: Input;
const output = match(input)
.with(
{ prop: P.intersection(P.instanceOf(A), { foo: 'bar' }) },
({ prop }) => prop.foo // prop: A & { foo: 'bar' }
)
.with(
{ prop: P.intersection(P.instanceOf(A), { foo: 'baz' }) },
({ prop }) => prop.foo // prop: A & { foo: 'baz' }
)
.otherwise(() => '');
Các mệnh đề P.string
P.string
có một số phương pháp để giúp bạn khớp với các chuỗi cụ thể.
P.string.startsWith
P.string.startsWith(str)
khớp với các chuỗi bắt đầu bằng chuỗi được cung cấp.
const fn = (input: string) =>
match(input)
.with(P.string.startsWith('TS'), () => '🎉')
.otherwise(() => '❌');
console.log(fn('TS-Pattern')); // logs '🎉'
P.string.endsWith
P.string.endsWith(str)
khớp với các chuỗi kết thúc bằng chuỗi được cung cấp.
const fn = (input: string) =>
match(input)
.with(P.string.endsWith('!'), () => '🎉')
.otherwise(() => '❌');
console.log(fn('Hola!')); // logs '🎉'
P.string.minLength
P.string.minLength(min)
khớp với chuỗi có ít nhất min
ký tự.
const fn = (input: string) =>
match(input)
.with(P.string.minLength(2), () => '🎉')
.otherwise(() => '❌');
console.log(fn('two')); // logs '🎉'
P.string.maxLength
P.string.maxLength(max)
khớp với chuỗi có tối đa max
ký tự.
const fn = (input: string) =>
match(input)
.with(P.string.maxLength(5), () => '🎉')
.otherwise(() => 'too long');
console.log(fn('is this too long?')); // logs 'too long'
P.string.includes
P.string.includes(str)
khớp với chuỗi chứa chuỗi con được cung cấp.
const fn = (input: string) =>
match(input)
.with(P.string.includes('!'), () => '✅')
.otherwise(() => '❌');
console.log(fn('Good job! 🎉')); // logs '✅'
P.string.regex
P.string.regex(RegExp)
khớp với chuỗi nếu chúng phù hợp với biểu thức chính quy được cung cấp.
const fn = (input: string) =>
match(input)
.with(P.string.regex(/^[a-z]+$/), () => 'single word')
.otherwise(() => 'other strings');
console.log(fn('gabriel')); // logs 'single word'
Mệnh đề P.number
và P.bigint
P.number
và P.bigint
có một số phương pháp để giúp bạn khớp với các số và bigint cụ thể.
P.number.between
P.number.between(min, max)
khớp với các số nằm giữa min
và max
.
const fn = (input: number) =>
match(input)
.with(P.number.between(1, 5), () => '✅')
.otherwise(() => '❌');
console.log(fn(3), fn(1), fn(5), fn(7)); // logs '✅ ✅ ✅ ❌'
P.number.lt
P.number.lt(max)
khớp với các số nhỏ hơn max
.
const fn = (input: number) =>
match(input)
.with(P.number.lt(7), () => '✅')
.otherwise(() => '❌');
console.log(fn(2), fn(7)); // logs '✅ ❌'
P.number.gt
P.number.gt(min)
khớp với các số lớn hơn min
.
const fn = (input: number) =>
match(input)
.with(P.number.gt(7), () => '✅')
.otherwise(() => '❌');
console.log(fn(12), fn(7)); // logs '✅ ❌'
P.number.lte
P.number.lte(max)
khớp với các số nhỏ hơn hoặc bằng max
.
const fn = (input: number) =>
match(input)
.with(P.number.lte(7), () => '✅')
.otherwise(() => '❌');
console.log(fn(7), fn(12)); // logs '✅ ❌'
P.number.gte
P.number.gte(min)
khớp với các số lớn hơn hoặc bằng min
.
const fn = (input: number) =>
match(input)
.with(P.number.gte(7), () => '✅')
.otherwise(() => '❌');
console.log(fn(7), fn(2)); // logs '✅ ❌'
P.number.int
P.number.int()
khớp với số nguyên.
const fn = (input: number) =>
match(input)
.with(P.number.int(), () => '✅')
.otherwise(() => '❌');
console.log(fn(12), fn(-3.141592)); // logs '✅ ❌'
P.number.finite
P.number.finite()
khớp với tất cả các số trừ Infinity
và -Infinity
.
const fn = (input: number) =>
match(input)
.with(P.number.finite(), () => '✅')
.otherwise(() => '❌');
console.log(fn(-3.141592), fn(Infinity)); // logs '✅ ❌'
P.number.positive
P.number.positive()
khớp với các số dương.
const fn = (input: number) =>
match(input)
.with(P.number.positive(), () => '✅')
.otherwise(() => '❌');
console.log(fn(7), fn(-3.141592)); // logs '✅ ❌'
P.number.negative
P.number.negative()
khớp với các số âm.
const fn = (input: number) =>
match(input)
.with(P.number.negative(), () => '✅')
.otherwise(() => '❌');
console.log(fn(-3.141592), fn(7)); // logs '✅ ❌'
Loại dữ liệu
P.infer
P.infer<typeof somePattern>
cho phép bạn suy ra loại dữ liệu từ một mẫu cụ thể.
Điều này đặc biệt hữu ích khi xác minh một phản hồi API.
const postPattern = {
title: P.string,
content: P.string,
stars: P.number.between(1, 5).optional(),
author: {
firstName: P.string,
lastName: P.string.optional(),
followerCount: P.number,
},
} as const;
type Post = P.infer<typeof postPattern>;
// posts: Post[]
const posts = await fetch(someUrl)
.then((res) => res.json())
.then((res: unknown): Post[] =>
isMatching({ data: P.array(postPattern) }, res) ? res.data : []
);
Mặc dù không cần thiết một cách tuyệt đối, việc sử dụng as const
sau định nghĩa mẫu đảm bảo rằng TS-Pattern suy ra các loại chính xác nhất có thể.
P.narrow
P.narrow<Input, typeof Pattern>
sẽ thu hẹp loại đầu vào để chỉ giữ lại tập hợp các giá trị phù hợp với loại mẫu được cung cấp.
type Input = ['a' | 'b' | 'c', 'a' | 'b' | 'c'];
const Pattern = ['a', P.union('a', 'b')] as const;
type Narrowed = P.narrow<Input, typeof Pattern>;
// ^? ['a', 'a' | 'b']
Lưu ý rằng hầu hết thời gian, các hàm match
và isMatching
thực hiện việc thu hẹp dữ liệu dùm bạn, và bạn không cần phải thu hẹp loại dữ liệu bằng tay.
P.Pattern
P.Pattern<T>
là loại của tất cả các mẫu có thể cho một loại tổng quát T
.
type User = { name: string; age: number };
const userPattern: Pattern<User> = {
name: 'Alice',
};
Suy ra loại dữ liệu
TS-Pattern tận dụng một số tính năng tiên tiến nhất của hệ thống loại để thu hẹp loại đầu vào bằng cách sử dụng mẫu hiện tại. Nó cũng có khả năng biết chính xác xem bạn đã xử lý tất cả các trường hợp chưa, ngay cả khi so khớp trên cấu trúc dữ liệu phức tạp.
Dưới đây là một số ví dụ về tính năng suy ra của TS-Pattern.
Thu hẹp loại
Nếu bạn so khớp mẫu trên một loại tổng hợp với một thuộc tính phân biệt, TS-Pattern sẽ sử dụng thuộc tính phân biệt này để thu hẹp loại đầu vào.
type Text = { type: 'text'; data: string };
type Img = { type: 'img'; data: { src: string; alt: string } };
type Video = { type: 'video'; data: { src: string; format: 'mp4' | 'webm' } };
type Content = Text | Img | Video;
const formatContent = (content: Content): string =>
match(content)
.with({ type: 'text' }, (text /* : Text */) => '<p>...</p>')
.with({ type: 'img' }, (img /* : Img */) => '<img ... />')
.with({ type: 'video' }, (video /* : Video */) => '<video ... />')
.with(
{ type: 'img' },
{ type: 'video' },
(video /* : Img | Video */) => 'img or video'
)
.with(
{ type: P.union('img', 'video') },
(video /* : Img | Video */) => 'img or video'
)
.exhaustive();
Nếu bạn sử dụng P.select
, TS-Pattern sẽ chọn loại của thuộc tính bạn đã chọn và sẽ suy ra loại của hàm xử lý của bạn tương ứng.
const formatContent = (content: Content): string =>
match(content)
.with(
{ type: 'text', data: P.select() },
(content /* : string */) => '<p>...</p>'
)
.with(
{ type: 'video', data: { format: P.select() } },
(format /* : 'mp4' | 'webm' */) => '<video ... />'
)
.with(
{ type: P.union('img', 'video'), data: P.select() },
(data /* : Img['data'] | Video['data'] */) => 'img or video'
)
.exhaustive();
Nếu hàm được đưa cho P.when
là một Type Guard, TS-Pattern sẽ sử dụng loại trả về của hàm giữ nguyên để thu hẹp loại đầu vào.
const isString = (x: unknown): x is string => typeof x === 'string';
const isNumber = (x: unknown): x is number => typeof x === 'number';
const fn = (input: { id: number | string }) =>
match(input)
.with({ id: P.when(isString) }, (narrowed /* : { id: string } */) => 'yes')
.with({ id: P.when(isNumber) }, (narrowed /* : { id: number } */) => 'yes')
.exhaustive();
Kiểm tra tính toàn diện
Nếu cấu trúc dữ liệu của bạn chứa nhiều loại tổng hợp, bạn có thể so khớp mẫu trên nhiều loại tổng hợp đó với một mẫu đơn. TS-Pattern sẽ theo dõi các trường hợp đã được xử lý và các trường hợp chưa được xử lý, để bạn không bao giờ quên xử lý một trường hợp nào đó.
type Permission = 'editor' | 'viewer';
type Plan = 'basic' | 'pro';
const fn = (org: Plan, user: Permission): string =>
match([org, user])
.with(['basic', 'viewer'], () => {})
.with(['basic', 'editor'], () => {})
.with(['pro', 'viewer'], () => {})
// Fails with `NonExhaustiveError<['pro', 'editor']>`
// because the `['pro', 'editor']` case isn't handled.
.exhaustive();
Bạn muốn tìm hiểu cách TS-Pattern được xây dựng?
Hãy tham khảo 👉 Type-Level TypeScript, một khóa học trực tuyến để học cách tận dụng hoàn toàn các tính năng tiên tiến nhất của TypeScript!
Cảm hứng
Thư viện này đã được lấy cảm hứng mạnh mẽ từ bài viết tuyệt vời của Wim Jongeneel: Pattern Matching in TypeScript with Record and Wildcard Patterns. Nó đã khiến tôi nhận ra rằng so khớp mẫu có thể được thực hiện trong userland và chúng ta không cần phải chờ đợi nó được thêm vào ngôn ngữ chính thức. Tôi rất biết ơn về điều đó 🙏
Chi tiết Tải về:
Tác giả: Gvergnaud
Mã nguồn: https://github.com/gvergnaud/ts-pattern
Giấy phép: MIT license