TypeScript - Wie man einen generischen Typ basierend auf dem Objekteigenschaftstyp eingrenzt
Ich möchte einen TypeScript-Diensttyp ExtractOnProp erstellen, der ein Objekt aus einem Vereinigungstyp T extrahiert, dessen bestimmte Eigenschaft Prop einen gewünschten Typ ValueType hat.
type ExtractOnProp<T, Prop extends keyof T, ValueType> = /* ... */;
type Test1 = ExtractOnProp<
| { value: "test1" | "test2", prop1: any }
| { value: "test1", prop2: any }
| { value: "test3", prop3: any },
"value",
"test1">;
// Test1 == { value: "test1", prop1: any } | { value: "test1", prop2: any };
// Real use case is more complicated but example usage:
// We want to find an element from an array of possible values
function narrow<
T extends { value: string },
Value extends readonly T['value'][],
>(
elements: readonly T[],
filter: Value,
): ExtractOnProp<T, 'value', Value[number]> {
const result = elements.find((el) => filter.includes(el.value as any));
if (!result) {
throw new Error('not found');
}
return result as any;
}
narrow(
[
{ value: 'test1', prop1: 1 },
{ value: 'test2', prop1: 2 },
{ value: 'test3', prop3: 3 },
] as const,
['test1', 'test3'] as const,
);
// { value: 'test1', prop1: 1 } | { value: 'test3', prop3: 3 }
Hier sind die Dinge, die ich ausprobiert habe
type ExtractOnProp<T, Prop extends keyof T, ValueType> = T[Prop] extends ValueType ? T : never;
// Always returns never
// The closest I got was property type intersection
type IntersectPropType<T, Prop extends keyof T, ValueType> = T[Prop] extends infer S
? S extends ValueProp
? S
: never
: never;
type Test1 = IntersectPropType<{ value: 'test1' | 'test2' } | { value: "test3"; prop: any }, 'value', 'test2' | 'omitted'>;
// Test1 = 'test2';
Bearbeiten:
Ich bin jetzt näher gekommen, indem ich die Vereinigung eingegrenzt habe, aber nicht den Eigenschaftstyp selbst.
type ExtractOnProp<T, Prop extends keyof T, ValueType> = T extends {
[_ in Prop]: infer S;
}
? ValueType extends S
? T
: never
: never;
type Test = ExtractOnProp<
{ value: 'a' | 'c'; prop1: any } | { value: 'b'; prop2: any },
'value',
'a'
>;
// Test = { value: 'a' | 'c', prop1: any }
Antwort #1
Angesichts Ihrer angegebenen Anforderungen könnte ein Ansatz sein:
type ExtractOnProp<T, K extends keyof T, V> =
T extends unknown ? V extends T[K] ?
{ [P in keyof T]: P extends K ? T[P] & V : T[P] }
: never : never
Dies ist ein distributiver Bedingungstyp sowohl in T als auch in V. Das bedeutet, dass die Operation über Unionstypen in T und V distribuiert wird; T und V werden in ihre Union-Mitglieder aufgeteilt, der Typ wird für jedes dieser Mitglieder ausgewertet und das Ergebnis wird dann wieder zu einer Union zusammengeführt. Also wird ExtractOnProp<T1 | T2, K, V1 | V2> dasselbe sein wie ExtractOnProp<T1, K, V1> | ExtractOnProp<T2, K, V1> | ExtractOnProp<T1, K, V2> | ExtractOnProp<T2, K, V2>. Sie wollen dies auf jeden Fall mit T tun, da ein Teil des Ziels darin besteht, jedes Union-Mitglied von T separat zu untersuchen; und wenn Sie nicht über Unionen in T distribuieren, untersuchen Sie am Ende das gesamte T
Bedingte Typen sind nur auf diese Weise distributiv, wenn der geprüfte Typ ein generischer Typparameter ist. Der Typ V extends T[K] ? { [P in keyof T]: P extends K ? T[P] & V : T[P] } : never wäre in T nicht distributiv. Um dies zu erreichen, können wir ihn in das scheinbar nutzlose T extends unknown ? (...) : never einhüllen. Natürlich wird T unknown erweitern; wir wollen es nicht wirklich prüfen. Stattdessen wollen wir nur T in seine Vereinigungsmitglieder aufteilen, bevor wir den Rest des Typs auswerten.
Also, der Rest des Typs ist
V extends T[K] ? { [P in keyof T]: P extends K ? T[P] & V : T[P] } : never
Dies hat den Effekt, V auch in seine Union-Mitglieder aufzuteilen. Wenn das Mitglied von V nicht auf die Eigenschaft am Schlüssel K von T zuweisbar ist, wollen wir dieses T-Mitglied aus der Ausgabe entfernen (daher das never). Wenn es zuweisbar ist, wollen wir dieses Mitglied erfassen. Aber anstatt nur T zurückzugeben, mappen wir es zu {[P in keyof T]: P extends K ? T[P] & V : T[P] }. Dieser Typ ist im Grunde einfach T, abgesehen von der Eigenschaft bei K, die mit V geschnitten wird. Auf diese Weise wird die Eigenschaft, falls sie breiter als V ist, eingeschränkt.
Lass es uns ausprobieren:
type Test = ExtractOnProp<
{ value: "test1" | "test2", prop1: any } |
{ value: "test1", prop2: any } |
{ value: "test3", prop3: any },
"value", "test1">
// type Test =
// { value: "test1"; prop1: any; } |
// { value: "test1"; prop2: any; }
Sieht gut aus, das "test3"-Element wird vollständig entfernt, während das "test1" | "test2"-Element etwas eingeschränkt wird.
Nächster Test:
const ret = narrow([
{ value: 'test1', prop1: 1 },
{ value: 'test2', prop1: 2 },
{ value: 'test3', prop3: 3 },
] as const, ['test1', 'test3'] as const);
/* const ret: {
readonly value: "test1";
readonly prop1: 1;
readonly prop3?: undefined;
} | {
readonly value: "test3";
readonly prop3: 3;
readonly prop1?: undefined;
} */
Wieder scheint vernünftig. Mehr:
type Test1 = ExtractOnProp<
{ value: 'test1' | 'test2' } |
{ value: "test3"; prop: any },
'value', 'test2' | 'omitted'>;
// type Test1 = { value: "test2"; }
In Ordnung, denke ich? Der letzte:
type Test2 = ExtractOnProp<
{ value: 'a' | 'c'; prop1: any } |
{ value: 'b'; prop2: any },
'value',
'a'
>;
// type Test2 = { value: "a"; prop1: any;}
Okay, diese erledigen alle, was ich denke, dass Sie wollen. Solche Arten von Typmanipulationen scheinen immer viele Randfälle zu haben, daher wäre ich überrascht, wenn es nicht einige Funktion gibt, die sich anders verhält, als Sie möchten. Testen Sie also gründlich, und hoffentlich, wenn etwas nicht so ist, wie Sie es wollen, können Sie dies selbst ändern.
Playground-Link zum Code
