🐛 Narrow union-valued and optional discriminants
The tagged-union matcher selected handler parameters with
`Extract<T, Record<K, V>>`, which only keeps a member whose
discriminant is a singleton literal. A property that is itself a
union (`{ color: "red" | "green" | "blue" }`) or optional
(`{ type?: "x" }`) matched no member, so every handler received
`never`, and the fallback saw the whole shape instead of the
unhandled tags.
Replace the selector with `Narrowed`, which distributes over `T`,
drops a member whose `K` cannot take the tag, and narrows `K`. The
`T[K] extends V` fast path keeps an exact member (a discriminated
union's declared interface) untouched. The fallback reuses
`Narrowed` over `Exclude<Tags, HandledTags>`, so a union-valued
property narrows to the unhandled tags rather than the whole shape.
Matching a defined tag on an optional property makes the key
required (`{ type: "x" }`); the `undefined` tag yields
`{ type?: never }` under `exactOptionalPropertyTypes` (absence) or
`{ type?: undefined }` when the property admits an explicit
`undefined`.
This commit is contained in:
1 parent
e7b5c0284d
commit
55bf6c24a7
5 files changed
+357
-25
No files matched your search
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- narrow the tagged-union matcher's handler parameters for a union-valued or
|
||||||
|
optional discriminant, and its fallback to the unhandled tags, instead of
|
||||||
|
passing `never`
|
||||||
- prune the backlog of completed items and record the TS 7 LSP shutdown
|
- prune the backlog of completed items and record the TS 7 LSP shutdown
|
||||||
workaround in development/testing.md
|
workaround in development/testing.md
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -17,10 +17,10 @@ v1.0:
|
|||||||
☐ Achieve 100% branch coverage on `src/index.ts`
|
☐ Achieve 100% branch coverage on `src/index.ts`
|
||||||
|
|
||||||
Matcher:
|
Matcher:
|
||||||
☐ when using a union type as a property, the current behavior of tagged union matcher is
|
✔ when using a union type as a property, the current behavior of tagged union matcher is @done
|
||||||
to pass never to handler parameters
|
to pass never to handler parameters
|
||||||
→ new matcher function needed or can be fixed in tagged union matcher
|
→ new matcher function needed or can be fixed in tagged union matcher
|
||||||
☐ optional discriminant (`{ type?: "x" }`) is the same hole: the boolean/nullish change now admits the `undefined` tag, so the factory accepts the key, but `Extract<T, Record<K, V>>` still passes `never` to both the `x` and `undefined` handlers
|
✔ optional discriminant (`{ type?: "x" }`) is the same hole: the boolean/nullish change now admits the `undefined` tag, so the factory accepts the key, but `Extract<T, Record<K, V>>` still passes `never` to both the `x` and `undefined` handlers @done
|
||||||
|
|
||||||
Documentation:
|
Documentation:
|
||||||
☐ Bring README.md back to its previous form — synopsis and examples restored, in the correct place
|
☐ Bring README.md back to its previous form — synopsis and examples restored, in the correct place
|
||||||
|
|||||||
+17
-6
@@ -208,16 +208,27 @@ The key is a separate call because `K` is inferred from its literal argument and
|
|||||||
|
|
||||||
#### Why
|
#### Why
|
||||||
|
|
||||||
- **Same fallback/remainder machinery as the primitive-union matcher.** `HandledMembers`
|
- **Same fallback/remainder machinery as the primitive-union matcher.** `HandledTags`
|
||||||
maps the handled tags to their members and `Exclude<T, …>` is the fallback's
|
recovers the tag values the map handled (`Member<Tags, keyof Handled>`) and
|
||||||
parameter; the redundant-fallback guard is the same F-bounded constraint. Only
|
`Narrowed<T, K, Exclude<Tags, …>>` is the fallback's parameter; the
|
||||||
the "universe" changes — `T`'s members instead of primitive values.
|
redundant-fallback guard is the same F-bounded constraint. Only the "universe"
|
||||||
|
changes — `T`'s members instead of primitive values.
|
||||||
- **`T extends object`, not `Record<PropertyKey, unknown>`.** An `interface` has
|
- **`T extends object`, not `Record<PropertyKey, unknown>`.** An `interface` has
|
||||||
no implicit index signature, so the `Record` constraint would reject
|
no implicit index signature, so the `Record` constraint would reject
|
||||||
interface-based unions. The runtime reads the tag off `object` with one
|
interface-based unions. The runtime reads the tag off `object` with one
|
||||||
assertion, the tagged twin of the primitive-union dispatch's `shape as string | number`.
|
assertion, the tagged twin of the primitive-union dispatch's `shape as string | number`.
|
||||||
- **`MapTaggedUnion` distributes with `Extract`.** A duplicated tag yields a
|
- **`Narrowed` distributes over `T`, narrowing `K` to the tag.** A member whose
|
||||||
union of members instead of dropping one.
|
`K` cannot take the tag drops out; a duplicated tag yields a union of members
|
||||||
|
instead of dropping one. `T[K] extends V` returns the exact member (a
|
||||||
|
discriminated union's declared interface) untouched, so the mapped form only
|
||||||
|
handles a property that is itself a union.
|
||||||
|
- **A union-valued or optional discriminant is supported.** A single shape whose
|
||||||
|
property is a union (`{ color: "red" | "green" | "blue" }`) is narrowed per
|
||||||
|
handler instead of being passed `never`. Matching a defined tag on an optional
|
||||||
|
property (`{ type?: "x" }`) proves the key is present, so it becomes required
|
||||||
|
(`{ type: "x" }`); the `undefined` tag narrows it to `{ type?: never }` under
|
||||||
|
`exactOptionalPropertyTypes` (absence) or `{ type?: undefined }` when the
|
||||||
|
property explicitly admits `undefined`.
|
||||||
- **A `boolean` / `null` / `undefined` tag goes through the shared
|
- **A `boolean` / `null` / `undefined` tag goes through the shared
|
||||||
`PatternKey` / `Member` projection.** `Discriminated` admits those tags
|
`PatternKey` / `Member` projection.** `Discriminated` admits those tags
|
||||||
(they are in `Tag`), but they cannot key a mapped type, so the handler map is
|
(they are in `Tag`), but they cannot key a mapped type, so the handler map is
|
||||||
|
|||||||
@@ -892,6 +892,309 @@ test("getTaggedUnionMatcher: the maximum illegal tag universe is rejected", () =
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Union-valued and optional discriminants
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
test("getTaggedUnionMatcher: a union-valued property narrows per handler", () => {
|
||||||
|
// Arrange — one shape, not a union of variants; `color` is a union.
|
||||||
|
interface Paint {
|
||||||
|
readonly color: "red" | "green" | "blue";
|
||||||
|
readonly value: number;
|
||||||
|
}
|
||||||
|
const factory = getTaggedUnionMatcher<Paint>()("color");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const describe = factory({
|
||||||
|
red: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{
|
||||||
|
readonly color: "red";
|
||||||
|
readonly value: number;
|
||||||
|
}>();
|
||||||
|
assert.equal(s.color, "red");
|
||||||
|
return s.value;
|
||||||
|
},
|
||||||
|
green: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{
|
||||||
|
readonly color: "green";
|
||||||
|
readonly value: number;
|
||||||
|
}>();
|
||||||
|
assert.equal(s.color, "green");
|
||||||
|
return s.value;
|
||||||
|
},
|
||||||
|
blue: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{
|
||||||
|
readonly color: "blue";
|
||||||
|
readonly value: number;
|
||||||
|
}>();
|
||||||
|
assert.equal(s.color, "blue");
|
||||||
|
return s.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(describe).toEqualTypeOf<(shape: Paint) => number>();
|
||||||
|
assert.equal(describe({ color: "red", value: 1 }), 1);
|
||||||
|
assert.equal(describe({ color: "green", value: 2 }), 2);
|
||||||
|
assert.equal(describe({ color: "blue", value: 3 }), 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTaggedUnionMatcher: a fallback narrows a union-valued property", () => {
|
||||||
|
// Arrange
|
||||||
|
interface Paint {
|
||||||
|
color: "red" | "green" | "blue";
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
const factory = getTaggedUnionMatcher<Paint>()("color");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const describe = factory(
|
||||||
|
{
|
||||||
|
red: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{
|
||||||
|
color: "red";
|
||||||
|
value: number;
|
||||||
|
}>();
|
||||||
|
assert.equal(s.color, "red");
|
||||||
|
return 1 as const;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{
|
||||||
|
color: "green" | "blue";
|
||||||
|
value: number;
|
||||||
|
}>();
|
||||||
|
assert.ok(s.color === "green" || s.color === "blue");
|
||||||
|
return 2 as const;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(describe).toEqualTypeOf<(shape: Paint) => 1 | 2>();
|
||||||
|
assert.equal(describe({ color: "red", value: 1 }), 1);
|
||||||
|
assert.equal(describe({ color: "green", value: 2 }), 2);
|
||||||
|
assert.equal(describe({ color: "blue", value: 3 }), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTaggedUnionMatcher: a numeric union-valued property narrows per handler", () => {
|
||||||
|
// Arrange
|
||||||
|
interface Version {
|
||||||
|
code: 1 | 2 | 3;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
const factory = getTaggedUnionMatcher<Version>()("code");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const describe = factory({
|
||||||
|
1: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ code: 1; value: number }>();
|
||||||
|
assert.equal(s.code, 1);
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
2: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ code: 2; value: number }>();
|
||||||
|
assert.equal(s.code, 2);
|
||||||
|
return 2;
|
||||||
|
},
|
||||||
|
3: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ code: 3; value: number }>();
|
||||||
|
assert.equal(s.code, 3);
|
||||||
|
return 3;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(describe).toEqualTypeOf<(shape: Version) => number>();
|
||||||
|
assert.equal(describe({ code: 1, value: 10 }), 1);
|
||||||
|
assert.equal(describe({ code: 2, value: 20 }), 2);
|
||||||
|
assert.equal(describe({ code: 3, value: 30 }), 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTaggedUnionMatcher: an optional discriminant narrows both handlers", () => {
|
||||||
|
// Arrange — `type?` is `"x" | undefined`; both handlers must be typed,
|
||||||
|
// and matching `"x"` proves the key is present, so `type` becomes required.
|
||||||
|
interface Maybe {
|
||||||
|
type?: "x";
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
const factory = getTaggedUnionMatcher<Maybe>()("type");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const describe = factory({
|
||||||
|
x: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ type: "x"; value: number }>();
|
||||||
|
assert.equal(s.type, "x");
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
undefined: (s) => {
|
||||||
|
// The input only allows absence (exactOptionalPropertyTypes), so
|
||||||
|
// the `undefined` tag narrows the key to never-present.
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ type?: never; value: number }>();
|
||||||
|
assert.equal(s.type, undefined);
|
||||||
|
return 2;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(describe).toEqualTypeOf<(shape: Maybe) => number>();
|
||||||
|
assert.equal(describe({ type: "x", value: 1 }), 1);
|
||||||
|
assert.equal(describe({ value: 2 }), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTaggedUnionMatcher: an optional discriminant fallback keeps the undefined tag", () => {
|
||||||
|
// Arrange
|
||||||
|
interface Maybe {
|
||||||
|
type?: "x";
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
const factory = getTaggedUnionMatcher<Maybe>()("type");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const describe = factory(
|
||||||
|
{
|
||||||
|
x: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ type: "x"; value: number }>();
|
||||||
|
assert.equal(s.type, "x");
|
||||||
|
return 1 as const;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{
|
||||||
|
type?: never;
|
||||||
|
value: number;
|
||||||
|
}>();
|
||||||
|
assert.equal(s.type, undefined);
|
||||||
|
return 2 as const;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(describe).toEqualTypeOf<(shape: Maybe) => 1 | 2>();
|
||||||
|
assert.equal(describe({ type: "x", value: 1 }), 1);
|
||||||
|
assert.equal(describe({ value: 2 }), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTaggedUnionMatcher: explicit `undefined` stays distinct from absence", () => {
|
||||||
|
// Arrange — `type?: "x" | undefined` admits an explicit `undefined`, unlike
|
||||||
|
// the bare optional above; the two representations must not be conflated.
|
||||||
|
interface Explicit {
|
||||||
|
type?: "x" | undefined;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
const factory = getTaggedUnionMatcher<Explicit>()("type");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const describe = factory({
|
||||||
|
x: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ type: "x"; value: number }>();
|
||||||
|
assert.equal(s.type, "x");
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
undefined: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{
|
||||||
|
type?: undefined;
|
||||||
|
value: number;
|
||||||
|
}>();
|
||||||
|
assert.equal(s.type, undefined);
|
||||||
|
return 2;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(describe).toEqualTypeOf<(shape: Explicit) => number>();
|
||||||
|
assert.equal(describe({ type: "x", value: 1 }), 1);
|
||||||
|
assert.equal(describe({ type: undefined, value: 2 }), 2);
|
||||||
|
assert.equal(describe({ value: 3 }), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTaggedUnionMatcher: a union-valued member is split, not dropped", () => {
|
||||||
|
// A member whose tag is itself a union sits beside a singleton-tag member.
|
||||||
|
// Selecting members with `Extract<T, Record<K, V>>` keeps only the
|
||||||
|
// singleton; the distributive narrowing must keep both sides of the
|
||||||
|
// union-valued member.
|
||||||
|
// Arrange
|
||||||
|
type Mixed = { kind: "a"; a: number } | { kind: "a" | "b"; b: number };
|
||||||
|
const factory = getTaggedUnionMatcher<Mixed>()("kind");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const describe = factory({
|
||||||
|
a: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<
|
||||||
|
{ kind: "a"; a: number } | { kind: "a"; b: number }
|
||||||
|
>();
|
||||||
|
assert.equal(s.kind, "a");
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
b: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ kind: "b"; b: number }>();
|
||||||
|
assert.equal(s.kind, "b");
|
||||||
|
return 2;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(describe).toEqualTypeOf<(shape: Mixed) => number>();
|
||||||
|
assert.equal(describe({ kind: "a", a: 1 }), 1);
|
||||||
|
assert.equal(describe({ kind: "a", b: 2 }), 1);
|
||||||
|
assert.equal(describe({ kind: "b", b: 3 }), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTaggedUnionMatcherW: a union-valued property widens to the union of returns", () => {
|
||||||
|
// Arrange
|
||||||
|
interface Paint {
|
||||||
|
color: "red" | "green" | "blue";
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
const factory = getTaggedUnionMatcherW<Paint>()("color");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const describe = factory({
|
||||||
|
red: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ color: "red"; value: number }>();
|
||||||
|
assert.equal(s.color, "red");
|
||||||
|
return "r" as const;
|
||||||
|
},
|
||||||
|
green: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ color: "green"; value: number }>();
|
||||||
|
assert.equal(s.color, "green");
|
||||||
|
return 1 as const;
|
||||||
|
},
|
||||||
|
blue: (s) => {
|
||||||
|
expectTypeOf(s).toEqualTypeOf<{ color: "blue"; value: number }>();
|
||||||
|
assert.equal(s.color, "blue");
|
||||||
|
return true as const;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expectTypeOf(describe).toEqualTypeOf<(shape: Paint) => "r" | 1 | true>();
|
||||||
|
assert.equal(describe({ color: "red", value: 1 }), "r");
|
||||||
|
assert.equal(describe({ color: "green", value: 2 }), 1);
|
||||||
|
assert.equal(describe({ color: "blue", value: 3 }), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getTaggedUnionMatcher: a union-valued property still enforces its contract", () => {
|
||||||
|
// Arrange
|
||||||
|
interface Paint {
|
||||||
|
color: "red" | "green" | "blue";
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
const factory = getTaggedUnionMatcher<Paint>()("color");
|
||||||
|
|
||||||
|
// Act / Assert — the calls below must not compile
|
||||||
|
factory({
|
||||||
|
red: () => 1,
|
||||||
|
green: () => 2,
|
||||||
|
blue: () => 3,
|
||||||
|
// @ts-expect-error `yellow` is not a value of `color`
|
||||||
|
yellow: () => 4,
|
||||||
|
});
|
||||||
|
// @ts-expect-error a gap without a fallback is not exhaustive
|
||||||
|
factory({ red: () => 1 });
|
||||||
|
// @ts-expect-error a fallback is redundant once every value is handled
|
||||||
|
factory({ red: () => 1, green: () => 2, blue: () => 3 }, () => 0);
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Dispatch — runtime behavior
|
// Dispatch — runtime behavior
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
+32
-17
@@ -1,4 +1,4 @@
|
|||||||
import type { Exact, UnknownRecord } from "type-fest";
|
import type { Exact, SetRequired, UnknownRecord } from "type-fest";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
HandlerMap,
|
HandlerMap,
|
||||||
@@ -28,34 +28,49 @@ type Discriminated<T extends object> = {
|
|||||||
[K in keyof T]: T[K] extends Matchable ? K : never;
|
[K in keyof T]: T[K] extends Matchable ? K : never;
|
||||||
}[keyof T];
|
}[keyof T];
|
||||||
|
|
||||||
// The member(s) of `T` tagged `V`. `Extract` distributes over the union, so a
|
// The member(s) of `T` narrowed to the tag(s) `V`. Distributes over `T`, so a
|
||||||
// duplicated tag maps to a union of members rather than silently dropping one.
|
// duplicated tag maps to a union of members rather than silently dropping one.
|
||||||
|
// A member whose `K` cannot take `V` drops out; the rest have `K` narrowed to
|
||||||
|
// `Extract<T[K], V>`. `T[K] extends V` keeps an exact member (a discriminated
|
||||||
|
// union's declared interface) untouched, so only a property that is itself a
|
||||||
|
// union — or optional — goes through the mapped form. `SetRequired` makes `K`
|
||||||
|
// required unless `V` admits `undefined`: a defined tag yields `{ type: "x" }`,
|
||||||
|
// while the `undefined` tag keeps `{ type?: never }` (absence) or
|
||||||
|
// `{ type?: undefined }` when the property admits an explicit `undefined`.
|
||||||
// Keyed by `PatternKey`, so `boolean`/`null`/`undefined` tags can key a mapped
|
// Keyed by `PatternKey`, so `boolean`/`null`/`undefined` tags can key a mapped
|
||||||
// type; `Member` inverts the projection against the tag set to recover the
|
// type; `Member` inverts the projection against the tag set to recover the tag.
|
||||||
// member.
|
type Narrowed<T extends object, K extends keyof T, V> = T extends object
|
||||||
|
? [Extract<T[K], V>] extends [never]
|
||||||
|
? never
|
||||||
|
: T[K] extends V
|
||||||
|
? T
|
||||||
|
: undefined extends V
|
||||||
|
? { [P in keyof T]: P extends K ? Extract<T[P], V> : T[P] }
|
||||||
|
: SetRequired<
|
||||||
|
{ [P in keyof T]: P extends K ? Extract<T[P], V> : T[P] },
|
||||||
|
K
|
||||||
|
>
|
||||||
|
: never;
|
||||||
|
|
||||||
type MapTaggedUnion<T extends object, K extends keyof T> = {
|
type MapTaggedUnion<T extends object, K extends keyof T> = {
|
||||||
[P in PatternKey<Tags<T, K>>]: Extract<T, Record<K, Member<Tags<T, K>, P>>>;
|
[P in PatternKey<Tags<T, K>>]: Narrowed<T, K, Member<Tags<T, K>, P>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Handlers<T extends object, K extends keyof T, R> = {
|
type Handlers<T extends object, K extends keyof T, R> = {
|
||||||
[P in PatternKey<Tags<T, K>>]: UnaryFn<MapTaggedUnion<T, K>[P], R>;
|
[P in PatternKey<Tags<T, K>>]: UnaryFn<MapTaggedUnion<T, K>[P], R>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The members `Handled` covers. Mapping over the projected keys keeps every
|
// The tag values whose `PatternKey` is handled.
|
||||||
// index within `MapTaggedUnion`'s keys, and the conditional drops a stray key
|
type HandledTags<T extends object, K extends keyof T, Handled> = Member<
|
||||||
// outside `T` so it cannot widen the remainder. The remainder is
|
Tags<T, K>,
|
||||||
// `Exclude<T, …>`, mirroring the primitive-union matcher's
|
keyof Handled
|
||||||
// `Exclude<T, Member<T, keyof Handled>>`.
|
>;
|
||||||
type HandledMembers<T extends object, K extends keyof T, Handled> = {
|
|
||||||
[P in PatternKey<Tags<T, K>>]: P extends keyof Handled
|
|
||||||
? MapTaggedUnion<T, K>[P]
|
|
||||||
: never;
|
|
||||||
}[PatternKey<Tags<T, K>>];
|
|
||||||
|
|
||||||
// The fallback is a *second argument*, not a property of the handler map, so
|
// The fallback is a *second argument*, not a property of the handler map, so
|
||||||
// its parameter can be the remainder the map left uncovered. See development/library.md.
|
// its parameter can be the remainder the map left uncovered: the members
|
||||||
|
// narrowed to the tags the map did not handle. See development/library.md.
|
||||||
type Fallback<T extends object, K extends keyof T, Handled, R> = UnaryFn<
|
type Fallback<T extends object, K extends keyof T, Handled, R> = UnaryFn<
|
||||||
Exclude<T, HandledMembers<T, K, Handled>>,
|
Narrowed<T, K, Exclude<Tags<T, K>, HandledTags<T, K, Handled>>>,
|
||||||
R
|
R
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user