Одна из широко используемых функций функционального программирования - сопоставление с образцом. К сожалению, в TypeScript для него пока нет языковой поддержки - тут на помощь приходят библиотеки, и это мой взгляд на это.
TypeMatcher
TypeMatcher - это крошечная библиотека javascript, разработанная для предоставления базовых конструкций сопоставления с образцом, и в этом посте я опишу, как это работает, и приведу несколько примеров использования.
ВНИМАНИЕ: сообщение обновлено для версии 0.9.2, критические изменения могут произойти в версии с незначительными обновлениями до версии 1.x.
Библиотека состоит из двух основных компонентов:
Матчеры
Matcher - это функция, которая проверяет соответствие входного значения типу:
type TypeMatcher<T> = (val: any) => val is T
Некоторые предоставленные сопоставители: isString
, isNumber
, isArrayOf
, isTuple1
. Тип TypeMatcher совместим с функциями lodash с той же целью, поэтому вы можете использовать их напрямую.
Соответствие DSL
Соответствующий DSL состоит из функций match
, caseWhen
и caseDefault
, используемых для создания обработчиков случаев и оценки выражения соответствия по заданному значению.
match(value, cases)
принимает входное значение, обработчик регистра с типом type MatchCase<A, R> = { map: (val: A) => R }
, и возвращает первый совпадающий результат или выдает ошибку, если совпадений нет.
caseWhen(matcher, fn)
используется для создания MatchCase<A, R>
экземпляров с использованием TypeMatcher<T>
и функций-обработчиков. Для обработки случая по умолчанию используйте caseDefault()
функцию или метод.
Установка
npm install --save [email protected]
Примеры
Соответствие точным значениям
Убедитесь, что ввод соответствует определенным значениям перечисления:
enum UserRole { Member = 0, Moderator = 1, Admin = 2 } const role: UserRole = match(20, caseWhen(isLiteral(UserRole.Member), _ => _). caseWhen(isLiteral(UserRole.Moderator), _ => _). caseWhen(isLiteral(UserRole.Admin), _ => _). caseDefault(() => UserRole.Member) )
Проверка исчерпываемости
Компилятор (tsc
) завершится успешно, если случаи покрывают все возможные входные значения:
enum UserRole { Member = 0, Moderator = 1, Admin = 2 } function role(): UserRole { return UserRole.Member } const roleStr: string = match(role(), caseWhen(isLiteral(UserRole.Member), _ => "member"). caseWhen(isLiteral(UserRole.Moderator), _ => "moderator"). caseWhen(isLiteral(UserRole.Admin), _ => "admin") )
Но когда мы удаляем один из случаев, например:
const roleStr: string = match(role(), caseWhen(isLiteral(UserRole.Member), _ => "member"). caseWhen(isLiteral(UserRole.Admin), _ => "admin") )
компилятор выйдет из строя:
error TS2345: Argument of type 'UserRole' is not assignable to parameter of type 'UserRole.Member | UserRole.Admin'. const roleStr: string = match(role(), ~~~~~~
Сопоставить поля объекта
Убедитесь, что в объекте ввода есть все поля, определенные вашим настраиваемым типом:
enum Gender { Male = 'M', Female = 'F' } /** * Our custom type for user profiles */ type User = { name: string, gender: Gender, age: number address?: string } /** * User type matcher object */ const isUser: TypeMatcher<User> = hasFields({ name: isString, gender: isEither(isLiteral(Gender.Male), isLiteral(Gender.Female)), age: isNumber, address: isOptional(isString) }) // now you can build a inline match expression const user: User = match({}, caseWhen(isUser, _ => _). caseDefault(() => { throw new Error("Invalid user object") }) ) // or define a decoder function then use it // you can return type you need, like Either or Validation disjuctions const User: (val: unknown) => User | null = matcher( caseWhen(isUser, _ => _). caseDefault(() => null) ) const user2: User | null = User({})
Сопоставить массивы
Убедитесь, что все значения массива соответствуют заданному типу:
const someNumbers: Array<number> = match([], caseWhen(isArrayOf(isNumber), _ => _). caseDefault(() => []) ) const arr: Array<string> = match(someNumbers, caseWhen(isArrayOf(isNumber), arr => arr.map(it => it.toString())) )
Сопоставить кортежи
Библиотека определяет сопоставители от isTuple1
до isTuple10
, этого должно хватить для всех :).
const t1: [number] = match([10], caseWhen(isTuple1(isNumber), _ => _) ) const t2: [number, string] = match(t1 as any, caseWhen( isTuple3(isFiniteNumber, isString, isBoolean), (t): [number, string] => [t[0], t[1]] // sometimes you will have to help type inference ) ) const t4: [string, number, boolean, 10] = match(t2 as any, caseWhen(isTuple4(isString, isNumber, isBoolean, isLiteral(10)), _ => _) )
Пользовательские сопоставители
Вы можете предоставить свои собственные реализации сопоставления, совместимые с типом TypeMatcher<T>
:
import * as _ from "lodash" function isValidGender(val: any): val is "M" | "F" { return val === "M" || val === "F" } const s: string = match(10 as any, caseWhen(isValidGender, g => `gender: ${g}`) .caseWhen(_.isArray, arr => arr.join(",")) .caseWhen(_.isString, _ => _) )
Предварительная сборка сопоставителя
По сути, выражение match - это оценка функции A => B
, где A
- наше входное значение, а B
- тип объединения типов результатов наблюдений, и мы можем построить эту функцию, используя конструктор matcher
, который также является полезно для кода, чувствительного к производительности, и вы хотите сэкономить несколько циклов процессора:
const stringify = matcher( caseWhen(isArrayOf(isNumber), _ => _.join(",")). caseWhen(isArrayOf(isBoolean), _ => _.map(b => (b ? "yes" : "no")).join(",")). caseWhen(isString, _ => _) ) const s: string = stringify([10])
Проверьте исходный код для всех определенных сопоставителей: https://github.com/lostintime/node-typematcher/blob/master/src/lib/matchers.ts.
Ограничения
Дисперсия типов обработчиков дел
Избегайте явной установки типа аргумента в функции обработчика caseWhen()
, пусть тип определяется компилятором. Вы можете установить более конкретный тип, но проверка даст вам более общий тип, и компилятор не выйдет из строя. Это вызвано функцией TypeScript Двувариантность параметров функции.
UPD: в Typescript v2.6 есть опция компилятора --strictFunctionTypes
, и если она включена, для этого кода:
match(8, caseWhen(isNumber, (n: 10) => "n is 10"))
теперь вы получите эту ошибку:
error TS2345: Argument of type '8' is not assignable to parameter of type '10'. match(8, caseWhen(isNumber, (n: 10) => "n is 10")) ~
УСТАРЕЛО: используйте caseDefault
в конце
Новый match DSL, представленный в [email protected]
, обеспечивает проверку полноты во время компиляции, поэтому этот код:
const x: "ten" | "twenty" = match(8 as any, caseWhen(isString, () => "ten") )
потерпит неудачу во время компиляции с:
error TS2322: Type 'string' is not assignable to type '"ten" | "twenty"'. const x: "ten" | "twenty" = match(8 as any, ~
Но вам все равно придется обрабатывать случай по умолчанию, когда ожидается тип результата any
(что настоятельно НЕ рекомендуется), в противном случае он может выйти из строя с ошибкой No match
во время выполнения.
const x: any = match(8 as any, caseWhen(isString, () => "ten"). caseDefault(() => "twenty") )
Ссылки
- Исходный код TypeMatcher
- Пакет TypeMatcher npm
- Поддержка сопоставления с образцом - предложение по поддержке языка TypeScript.
- ECMAScript Pattern Matching Syntax - предложение поддержки языка ES
- Поддержка некоторого неструктурного (номинального) сопоставления типов в TypeScript
Первоначально размещено здесь: https://lostintimedev.com/2017/09/06/typematcher-pattern-matching-library-for-typescript.html