typer - Bättre ergonomi för TypeScript union-avsmalnande typvaktfunktion
Vi har ett verktyg i vår kodbas för att generera en typvakt för att avgränsa en diskriminerad union:
export type ExtractBranchFromUnion<
UNION,
DISCRIMINANT extends keyof UNION,
BRANCH extends UNION[DISCRIMINANT],
> = UNION extends Record<DISCRIMINANT, BRANCH> ? UNION : never;
export function narrow<
UNION,
DISCRIMINANT extends keyof UNION,
BRANCH extends UNION[DISCRIMINANT],
>(
discriminate: DISCRIMINANT,
branch: BRANCH,
): (
item: UNION,
) => item is ExtractBranchFromUnion<UNION, DISCRIMINANT, BRANCH> {
return (item): item is ExtractBranchFromUnion<UNION, DISCRIMINANT, BRANCH> =>
item[discriminate] === branch;
}
Det kan enkelt användas i ett filter-anrop för att begränsa en array till ett specifikt unionmedlem, men det kräver att du kommer ihåg att lägga till as const efter ett strängliteral branch-argument, annars härleder det typen felaktigt och ger dig ett resultat som är för brett:
type A = {type: 'A'; a: string};
type B = {type: 'B'; b: string};
type SimpleUnion = A | B;
const arr: SimpleUnion[] = [];
// BAD: type is SimpleUnion[]
const badListOfBs = arr.filter(narrow('type', 'B'));
// GOOD: type is B[]
const goodListOfBs = arr.filter(narrow('type', 'B' as const));
Även om detta problem delvis kan lindras med färdiga konstanter,
const Types = {
'A': 'A',
'B': 'B',
} as const;
// OKAY: type is B[] but it requires a premade constant
const okayListOfBs = arr.filter(narrow('type', Types.B));
det förhindrar fortfarande inte problemet med att människor glömmer as const med ett bokstavligt värde och blir förvirrade över varför saker inte fungerar (samt det faktum att as const bara ser fult ut i de två kodblocken ovan).
Finns det ett sätt att få TS att automatiskt härleda en smalare typ när man tillhandahåller ett strängliteral i narrow som visas ovan? Eller åtminstone ett sätt att få ett snyggt felmeddelande att visas?
Playground-länk för all ovanstående kod.
Svar #1
Du behöver bara lägga till extra begränsning på Branch generiska:
export type ExtractBranchFromUnion<
Union,
Discriminant extends keyof Union,
Branch extends Union[Discriminant],
> = Union extends Record<Discriminant, Branch> ? Union : never;
export function narrow<
Union,
Discriminant extends keyof Union,
Branch extends Union[Discriminant] & PropertyKey, // <------ change is here
>(
discriminate: Discriminant,
Branch: Branch,
): (
item: Union,
) => item is ExtractBranchFromUnion<Union, Discriminant, Branch> {
return (item): item is ExtractBranchFromUnion<Union, Discriminant, Branch> =>
item[discriminate] === Branch;
}
type A = { type: 'A'; a: string };
type B = { type: 'B'; b: string };
type SimpleUnion = A | B;
const arr: SimpleUnion[] = [];
const badListOfBs = arr.filter(narrow('type', 'B')); // B[]
const goodListOfBs = arr.filter(narrow('type', 'A')); // A[]
Lekplats
Jag har lagt till detta: Branch extends Union[Discriminant] & PropertyKey.
Jag skulle vilja satsa på att diskriminatorvärdet bör vara string | number | symbol
Som du kanske har märkt har jag kapitaliserat alla generiska namn eftersom jag tyckte att det var svårt att läsa. Detta är dock en fråga om preferenser.
