diff --git a/src/index.ts b/src/index.ts index 096a964..1f4b91d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1 @@ -export { - getPrimitiveUnionMatcher, - getPrimitiveUnionMatcherPartial, - getPrimitiveUnionMatcherPartialW, - getPrimitiveUnionMatcherW, -} from "./primitive.ts"; +export { strict, widened } from "./primitive.ts"; diff --git a/src/primitive.test.ts b/src/primitive.test.ts index ee9f57e..6f86202 100644 --- a/src/primitive.test.ts +++ b/src/primitive.test.ts @@ -1,50 +1,49 @@ /* oxlint-disable typescript/no-floating-promises -- expectTypeOf() is a sync type-assertion library that the type-aware linter misidentifies as a promise */ import { strict as assert } from "node:assert"; +import path from "node:path"; import { test } from "node:test"; import { expectTypeOf } from "expect-type"; import { - getPrimitiveUnionMatcher, - getPrimitiveUnionMatcherPartial, - getPrimitiveUnionMatcherPartialW, - getPrimitiveUnionMatcherW, -} from "./primitive.ts"; + type CompletionTarget, + LspSession, +} from "#test-utils/lsp-completion.ts"; + +import { strict, widened } from "./primitive.ts"; // ============================================================================ -// API: getPrimitiveUnionMatcherW — ✔️ Exhaustive / ❌ ReturnsStrict +// API: strict — ✔️ Exhaustive / ✔️ ReturnsStrict // ============================================================================ -test("getPrimitiveUnionMatcherW requires every literal key", () => { +test("strict: exhaustive pattern infers one common return type", () => { // Arrange - const factory = getPrimitiveUnionMatcherW<"a" | "b">(); + const factory = strict<"a" | "b">(); // Act const matcher = factory({ - a: (s) => { + a: (s): number => { expectTypeOf(s).toEqualTypeOf<"a">(); - return 1 as const; + return 1; }, - b: (s) => { + b: (s): 1 | 2 => { expectTypeOf(s).toEqualTypeOf<"b">(); - return "two" as const; + return 2; }, }); // Assert - // ❌ ReturnsStrict: mixed handler returns widen to their union. - expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b") => 1 | "two">(); - // A pattern missing a key must not satisfy the parameter type. - expectTypeOf<{ - a: () => number; - }>().not.toExtend[0]>(); + // ✔️ ReturnsStrict: R is the best common return type, not a widening union. + expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b") => number>(); + // ❌ a value outside T must not be accepted by the matcher + expectTypeOf<"c">().not.toExtend[0]>(); assert.equal(matcher("a"), 1); - assert.equal(matcher("b"), "two"); + assert.equal(matcher("b"), 2); }); -test("getPrimitiveUnionMatcherW dispatches on numeric literal keys", () => { +test("strict: dispatches on numeric literal keys", () => { // Arrange - const factory = getPrimitiveUnionMatcherW<1 | 2>(); + const factory = strict<1 | 2>(); // Act const matcher = factory({ @@ -64,9 +63,150 @@ test("getPrimitiveUnionMatcherW dispatches on numeric literal keys", () => { assert.equal(matcher(2), 20); }); -test("getPrimitiveUnionMatcherW dispatches on mixed string and numeric keys", () => { +test("strict: dispatches on mixed string and numeric keys", () => { // Arrange - const factory = getPrimitiveUnionMatcherW<"a" | "b" | 1 | 2>(); + const factory = strict<"a" | "b" | 1 | 2>(); + + // Act + const matcher = factory({ + a: (s): number => { + expectTypeOf(s).toEqualTypeOf<"a">(); + return 1; + }, + b: (s): 1 | 2 => { + expectTypeOf(s).toEqualTypeOf<"b">(); + return 2; + }, + 1: (n): number => { + expectTypeOf(n).toEqualTypeOf<1>(); + return 10; + }, + 2: (n): number => { + expectTypeOf(n).toEqualTypeOf<2>(); + return 20; + }, + }); + + // Assert + expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | 1 | 2) => number>(); + assert.equal(matcher("a"), 1); + assert.equal(matcher("b"), 2); + assert.equal(matcher(1), 10); + assert.equal(matcher(2), 20); +}); + +// ============================================================================ +// API: strict — ❌ Exhaustive (fallback) / ✔️ ReturnsStrict +// ============================================================================ + +test("strict: a `_` fallback makes the universe keys optional", () => { + // Arrange + const factory = strict<"a" | "b" | "c">(); + + // Act + const matcher = factory({ + a: (s): 1 | 2 => { + expectTypeOf(s).toEqualTypeOf<"a">(); + return 1; + }, + _: (s): 1 | 2 => { + // The fallback sees the whole union, not a single literal. + expectTypeOf(s).toEqualTypeOf<"a" | "b" | "c">(); + return 2; + }, + }); + + // Assert + expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | "c") => 1 | 2>(); + // ❌ a value outside T must not be accepted by the matcher + expectTypeOf<"d">().not.toExtend[0]>(); + assert.equal(matcher("a"), 1); + assert.equal(matcher("b"), 2); + assert.equal(matcher("c"), 2); +}); + +test("strict: a `_` fallback also accepts an exhaustive pattern", () => { + // Arrange + const factory = strict<"a" | "b">(); + + // Act + const matcher = factory({ + a: (s) => { + expectTypeOf(s).toEqualTypeOf<"a">(); + return "A"; + }, + b: (s) => { + expectTypeOf(s).toEqualTypeOf<"b">(); + return "B"; + }, + }); + + // Assert + expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b") => string>(); + assert.equal(matcher("a"), "A"); + assert.equal(matcher("b"), "B"); +}); + +test("strict: a `_` fallback routes mixed string and numeric gaps", () => { + // Arrange + const factory = strict<"a" | "b" | 1 | 2>(); + + // Act + const matcher = factory({ + a: (s): string => { + expectTypeOf(s).toEqualTypeOf<"a">(); + return "A"; + }, + 1: (n): string => { + expectTypeOf(n).toEqualTypeOf<1>(); + return "one"; + }, + _: (s): string => { + expectTypeOf(s).toEqualTypeOf<"a" | "b" | 1 | 2>(); + return "fallback"; + }, + }); + + // Assert + expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | 1 | 2) => string>(); + assert.equal(matcher("a"), "A"); + assert.equal(matcher("b"), "fallback"); + assert.equal(matcher(1), "one"); + assert.equal(matcher(2), "fallback"); +}); + +// ============================================================================ +// API: widened — ✔️ Exhaustive / ❌ ReturnsStrict +// ============================================================================ + +test("widened: exhaustive pattern widens to the union of handler returns", () => { + // Arrange + const factory = widened<"x" | "y">(); + + // Act + const matcher = factory({ + x: (s) => { + expectTypeOf(s).toEqualTypeOf<"x">(); + return 1 as const; + }, + y: (s) => { + expectTypeOf(s).toEqualTypeOf<"y">(); + return "two" as const; + }, + }); + + // Assert + // ❌ ReturnsStrict: mixed handler returns widen to their union. + expectTypeOf(matcher).toEqualTypeOf<(shape: "x" | "y") => 1 | "two">(); + // ❌ a value outside T must not be accepted by the matcher + expectTypeOf<"z">().not.toExtend[0]>(); + assert.equal(matcher("x"), 1); + assert.equal(matcher("y"), "two"); +}); + +test("widened: dispatches on mixed string and numeric keys", () => { + // Arrange + const factory = widened<"a" | "b" | 1 | 2>(); // Act const matcher = factory({ @@ -99,181 +239,12 @@ test("getPrimitiveUnionMatcherW dispatches on mixed string and numeric keys", () }); // ============================================================================ -// API: getPrimitiveUnionMatcher — ✔️ Exhaustive / ✔️ ReturnsStrict +// API: widened — ❌ Exhaustive (fallback) / ❌ ReturnsStrict // ============================================================================ -test("getPrimitiveUnionMatcher infers a single return type shared by all handlers", () => { +test("widened: a `_` fallback widens gaps into the union", () => { // Arrange - const factory = getPrimitiveUnionMatcher<"a" | "b">(); - - // Act - const matcher = factory({ - a: (s): number => { - expectTypeOf(s).toEqualTypeOf<"a">(); - return 1; - }, - b: (s): 1 | 2 => { - expectTypeOf(s).toEqualTypeOf<"b">(); - return 2; - }, - }); - - // Assert - // ✔️ ReturnsStrict: R is the best common return type, not a widening union. - expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b") => number>(); - assert.equal(matcher("a"), 1); - assert.equal(matcher("b"), 2); -}); - -test("getPrimitiveUnionMatcher handlers receive the matched literal", () => { - // Arrange - const factory = getPrimitiveUnionMatcher<"on" | "off">(); - - // Act - const matcher = factory({ - on: (s) => { - expectTypeOf(s).toEqualTypeOf<"on">(); - return `handler ${s}`; - }, - off: (s) => { - expectTypeOf(s).toEqualTypeOf<"off">(); - return `handler ${s}`; - }, - }); - - // Assert - expectTypeOf(matcher).toEqualTypeOf<(shape: "on" | "off") => string>(); - assert.equal(matcher("on"), "handler on"); - assert.equal(matcher("off"), "handler off"); -}); - -test("getPrimitiveUnionMatcher infers one return type across mixed string and numeric keys", () => { - // Arrange - const factory = getPrimitiveUnionMatcher<"a" | "b" | 1 | 2>(); - - // Act - const matcher = factory({ - a: (s): number => { - expectTypeOf(s).toEqualTypeOf<"a">(); - return 1; - }, - b: (s): 1 | 2 => { - expectTypeOf(s).toEqualTypeOf<"b">(); - return 2; - }, - 1: (n): number => { - expectTypeOf(n).toEqualTypeOf<1>(); - return 10; - }, - 2: (n): number => { - expectTypeOf(n).toEqualTypeOf<2>(); - return 20; - }, - }); - - // Assert - // ✔️ ReturnsStrict: R is the best common return type, not a widening union. - expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | 1 | 2) => number>(); - assert.equal(matcher("a"), 1); - assert.equal(matcher("b"), 2); - assert.equal(matcher(1), 10); - assert.equal(matcher(2), 20); -}); - -// ============================================================================ -// API: getPrimitiveUnionMatcherPartial — ❌ Exhaustive / ✔️ ReturnsStrict -// ============================================================================ - -test("getPrimitiveUnionMatcherPartial routes shapes without a handler to _", () => { - // Arrange - const factory = getPrimitiveUnionMatcherPartial<"a" | "b" | "c">(); - // Two keys, so `{ a }` lacks only the `_` fallback, nothing else. - const sparseFactory = getPrimitiveUnionMatcherPartial<"a" | "b">(); - - // Act - const matcher = factory({ - a: (s): 1 | 2 => { - expectTypeOf(s).toEqualTypeOf<"a">(); - return 1; - }, - _: (s): 1 | 2 => { - // The fallback sees the whole union, not a single literal. - expectTypeOf(s).toEqualTypeOf<"a" | "b" | "c">(); - return 2; - }, - }); - - // Assert - expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | "c") => 1 | 2>(); - // ❌ Exhaustive: gaps are allowed, but only with a `_` fallback. - expectTypeOf<{ - a: () => number; - }>().not.toExtend[0]>(); - assert.equal(matcher("a"), 1); - assert.equal(matcher("b"), 2); - assert.equal(matcher("c"), 2); -}); - -test("getPrimitiveUnionMatcherPartial also accepts an exhaustive pattern", () => { - // Arrange - const factory = getPrimitiveUnionMatcherPartial<"a" | "b">(); - - // Act - const matcher = factory({ - a: (s) => { - expectTypeOf(s).toEqualTypeOf<"a">(); - return "A"; - }, - b: (s) => { - expectTypeOf(s).toEqualTypeOf<"b">(); - return "B"; - }, - }); - - // Assert - expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b") => string>(); - assert.equal(matcher("a"), "A"); - assert.equal(matcher("b"), "B"); -}); - -test("getPrimitiveUnionMatcherPartial routes mixed string and numeric keys, gaps go to _", () => { - // Arrange - const factory = getPrimitiveUnionMatcherPartial<"a" | "b" | 1 | 2>(); - - // Act - const matcher = factory({ - a: (s): string => { - expectTypeOf(s).toEqualTypeOf<"a">(); - return "A"; - }, - 1: (n): string => { - expectTypeOf(n).toEqualTypeOf<1>(); - return "one"; - }, - _: (s): string => { - // The fallback sees the whole union, not a single literal. - expectTypeOf(s).toEqualTypeOf<"a" | "b" | 1 | 2>(); - return "fallback"; - }, - }); - - // Assert - expectTypeOf(matcher).toEqualTypeOf<(shape: "a" | "b" | 1 | 2) => string>(); - assert.equal(matcher("a"), "A"); - assert.equal(matcher("b"), "fallback"); - assert.equal(matcher(1), "one"); - assert.equal(matcher(2), "fallback"); -}); - -// ============================================================================ -// API: getPrimitiveUnionMatcherPartialW — ❌ Exhaustive / ❌ ReturnsStrict -// ============================================================================ - -test("getPrimitiveUnionMatcherPartialW allows gaps and widens to the union of handler returns", () => { - // Arrange - const factory = getPrimitiveUnionMatcherPartialW<"x" | "y" | "z">(); - // Two keys, so `{ x }` lacks only the `_` fallback, nothing else. - const sparseFactory = getPrimitiveUnionMatcherPartialW<"x" | "y">(); + const factory = widened<"x" | "y" | "z">(); // Act const matcher = factory({ @@ -292,18 +263,16 @@ test("getPrimitiveUnionMatcherPartialW allows gaps and widens to the union of ha expectTypeOf(matcher).toEqualTypeOf< (shape: "x" | "y" | "z") => 1 | "fallback" >(); - // ❌ Exhaustive: gaps are allowed, but only with a `_` fallback. - expectTypeOf<{ - x: () => number; - }>().not.toExtend[0]>(); + // ❌ a value outside T must not be accepted by the matcher + expectTypeOf<"w">().not.toExtend[0]>(); assert.equal(matcher("x"), 1); assert.equal(matcher("y"), "fallback"); assert.equal(matcher("z"), "fallback"); }); -test("getPrimitiveUnionMatcherPartialW widens mixed string and numeric key returns to their union", () => { +test("widened: a `_` fallback widens mixed string and numeric returns", () => { // Arrange - const factory = getPrimitiveUnionMatcherPartialW<"a" | "b" | 1 | 2>(); + const factory = widened<"a" | "b" | 1 | 2>(); // Act const matcher = factory({ @@ -316,7 +285,6 @@ test("getPrimitiveUnionMatcherPartialW widens mixed string and numeric key retur return 10 as const; }, _: (s) => { - // The fallback sees the whole union, not a single literal. expectTypeOf(s).toEqualTypeOf<"a" | "b" | 1 | 2>(); return "fallback" as const; }, @@ -333,9 +301,9 @@ test("getPrimitiveUnionMatcherPartialW widens mixed string and numeric key retur assert.equal(matcher(2), "fallback"); }); -test("getPrimitiveUnionMatcherPartialW also accepts an exhaustive pattern", () => { +test("widened: a `_` fallback also accepts an exhaustive pattern", () => { // Arrange - const factory = getPrimitiveUnionMatcherPartialW<"x" | "y">(); + const factory = widened<"x" | "y">(); // Act const matcher = factory({ @@ -350,8 +318,140 @@ test("getPrimitiveUnionMatcherPartialW also accepts an exhaustive pattern", () = }); // Assert - // ❌ ReturnsStrict: mixed handler returns widen to their union. expectTypeOf(matcher).toEqualTypeOf<(shape: "x" | "y") => 1 | "two">(); assert.equal(matcher("x"), 1); assert.equal(matcher("y"), "two"); }); + +// ============================================================================ +// Factory contracts — calls that must not compile +// ============================================================================ + +test("strict factory rejects patterns outside its contract", () => { + // Arrange + const factory = strict<"a" | "b">(); + + // Act / Assert — the calls below must not compile. + // `Parameters[0]` resolves only the *last* overload, so it + // is not a sound "rejected" oracle (an accepted partial pattern does not + // extend it either); the call sites are the oracle instead. + factory({ + a: () => 1, + b: () => 2, + // @ts-expect-error `c` is not part of the universe `"a" | "b"` + c: () => 3, + }); + // @ts-expect-error a gap without `_` is not exhaustive + factory({ a: () => 1 }); +}); + +test("widened factory rejects patterns outside its contract", () => { + // Arrange + const factory = widened<"x" | "y">(); + + // Act / Assert — the calls below must not compile + // @ts-expect-error `z` is not part of the universe `"x" | "y"` + factory({ + x: () => 1 as const, + y: () => 2 as const, + z: () => 3 as const, + }); + // @ts-expect-error a gap without `_` is not exhaustive + factory({ x: () => 1 as const }); +}); + +// ============================================================================ +// Autocomplete — the language server is the oracle, not the type system +// ============================================================================ + +// Completion is a contextual-type property that the type system cannot observe, +// so the cases below read the popup from the repo's language server (via the +// test helper) rather than pairing `expectTypeOf` with `assert` — see +// development/testing.md § Autocomplete. They probe the *matcher's* overloads, +// which is why they live with the matcher and not with the helper. +const REPO_ROOT = path.resolve(import.meta.dirname, ".."); +const UNIVERSE = `"a" | "b" | "c"`; + +// One session per probe: the tests share no language-server state (open +// documents, project membership), so they pass in any order. +const labelsFor = ( + name: string, + factory: "strict" | "widened", + body: string, +): Promise => { + const session = new LspSession(REPO_ROOT); + const target: CompletionTarget = { + file: `src/__autocomplete_${name}.ts`, + source: [ + `import { ${factory} } from "./primitive.ts";`, + `const m = ${factory}<${UNIVERSE}>()({`, + body, + "});", + "", + ].join("\n"), + }; + return session + .completionLabelsAt(target) + .then((result) => result.labels) + .finally(() => session.close()); +}; + +test("autocomplete: an exhaustive pattern requires the universe, `_` optional", () => { + // Arrange + const name = "strict_fresh"; + + // Act + const labels = labelsFor(name, "strict", " /*COMPLETE*/"); + + // Assert + return labels.then((result) => { + assert.deepEqual([...result], ["_?", "a", "b", "c"]); + }); +}); + +test("autocomplete: handled keys drop out of the popup", () => { + // Arrange + const name = "strict_after_key"; + + // Act + const labels = labelsFor( + name, + "strict", + " a: () => 1,\n /*COMPLETE*/", + ); + + // Assert + return labels.then((result) => { + assert.deepEqual([...result], ["_?", "b", "c"]); + }); +}); + +test("autocomplete: `_` makes the remaining keys optional", () => { + // Arrange + const name = "strict_after_fallback"; + + // Act + const labels = labelsFor( + name, + "strict", + " _: () => 0,\n /*COMPLETE*/", + ); + + // Assert + return labels.then((result) => { + assert.deepEqual([...result], ["a?", "b?", "c?"]); + }); +}); + +test("autocomplete: `widened` offers the same popup as `strict`", () => { + // Arrange + const name = "widened_fresh"; + + // Act + const labels = labelsFor(name, "widened", " /*COMPLETE*/"); + + // Assert + return labels.then((result) => { + assert.deepEqual([...result], ["_?", "a", "b", "c"]); + }); +}); diff --git a/src/primitive.ts b/src/primitive.ts index 5046949..b5a8f55 100644 --- a/src/primitive.ts +++ b/src/primitive.ts @@ -1,71 +1,79 @@ -import type { Simplify, ValueOf } from "type-fest"; +import type { ValueOf } from "type-fest"; type UnaryFn = (shape: T) => R; -// ============================================================================ -// ✔️ Exhaustive -// ❌ ReturnsStrict -// ============================================================================ -type PatternPrimitiveUnion = { - [K in T]: UnaryFn; +// `Extract` drops optional handlers (`undefined`) so `PatternReturns` also works +// when `P`'s constraint has optional keys. +type PatternReturns

= ReturnType< + Extract, (...args: never[]) => unknown> +>; + +type Handlers = { [K in T]: UnaryFn }; + +// Fallback form: `_` is a required key, the T-keys are optional. +type Fallback = Partial> & { + _: UnaryFn; }; -type PatternReturns< - P extends Record>, -> = ReturnType>; +// Completion form: T-keys required, `_` optional. Overload #1, because +// TypeScript takes the *first* overload as the contextual type for the popup. +type ExhaustiveLoose = Handlers & { + _?: UnaryFn; +}; -export const getPrimitiveUnionMatcherW: () => < - P extends PatternPrimitiveUnion, ->( - pattern: Simplify

, -) => UnaryFn> = () => (pattern) => (shape) => - // Rewrite not to use any is possible, was evaluated and solutions were - // more complex than the current solution. +// The `P`-constraints are deliberately *closed* (no index signature): that is +// what makes the excess-property check reject keys outside `T`/`_`. +type ExhaustiveShape = Handlers & { + _?: UnaryFn; +}; +type FallbackShape = Partial< + Handlers +> & { + _: UnaryFn; +}; - // oxlint-disable-next-line typescript/no-explicit-any typescript/no-unsafe-type-assertion - (pattern[shape] as any)(shape); +// Strict returns: one common `R`. Overload order is load-bearing: +// #1 ExhaustiveLoose -> autocomplete `_?, a, b` +// #2 Fallback -> accepts a partial pattern +// #3 Handlers (last) -> "Property 'b' is missing" is the reported error +interface StrictFn { + (pattern: ExhaustiveLoose): UnaryFn; + (pattern: Fallback): UnaryFn; + (pattern: Handlers): UnaryFn; +} -// ============================================================================ -// ✔️ Exhaustive -// ✔️ ReturnsStrict -// ============================================================================ -export const getPrimitiveUnionMatcher: () => ( - pattern: Simplify>, -) => UnaryFn = getPrimitiveUnionMatcherW; +// Widened returns: the union of every handler's return type. `P` is the whole +// parameter (not an intersection), so its closed constraint both supplies the +// contextual/autocomplete type and enforces the excess-property check. +interface WidenedFn { +

>( + pattern: P & (keyof P extends T | "_" ? unknown : never), + ): UnaryFn>; +

>( + pattern: P & (keyof P extends T | "_" ? unknown : never), + ): UnaryFn>; +

>( + pattern: P & (keyof P extends T | "_" ? unknown : never), + ): UnaryFn>; +} -// ============================================================================ -// ❌ Exhaustive -// ✔️ ReturnsStrict -// ============================================================================ -type PatternPrimitiveUnionPartial = - | PatternPrimitiveUnion - | (Partial> & { - _: UnaryFn; - }); +type HandlerMap = Record | undefined>; -export const getPrimitiveUnionMatcherPartial: () => < - R, ->( - pattern: Simplify>, -) => UnaryFn = () => (pattern) => (shape) => - // Rewrite not to use any is possible, was evaluated and solutions were - // more complex than the current solution. +const dispatch = + (pattern: HandlerMap) => + (shape: string | number): unknown => { + const handler = pattern[shape] ?? pattern["_"]; + return handler === undefined + ? undefined + : Reflect.apply(handler, undefined, [shape]); + }; - // oxlint-disable-next-line typescript/no-explicit-any typescript/no-unsafe-type-assertion - (pattern[shape] ?? (pattern as any)["_"])(shape); +export const strict = + (): StrictFn => + (pattern: HandlerMap) => + dispatch(pattern); -// ============================================================================ -// ❌ Exhaustive -// ❌ ReturnsStrict -// ============================================================================ -export const getPrimitiveUnionMatcherPartialW: < - T extends string | number, ->() =>

>( - // `Simplify

` is the inference hook: callers infer `P` from the argument. - // the second half pins the impl parameter's `R` to `PatternReturns

`. - // that makes the `= getPrimitiveUnionMatcherPartial` assignment type-check. - // neither half works alone. - // without the witness the union's `_` arm demands `_ ∈ keyof P`. - // without `Simplify

` the parameter types do not compare. - pattern: Simplify

& PatternPrimitiveUnionPartial, T>, -) => UnaryFn> = getPrimitiveUnionMatcherPartial; +export const widened = + (): WidenedFn => + (pattern: HandlerMap) => + dispatch(pattern);