typescript - Hämta typen på ett objekt i "keyof" med en egenskap och generiska typer
Jag vill definiera ett gränssnitt med generisk typ som måste acceptera ett objekt där nycklarna är "root fields name" och värdet är en array av objekt som definierar några underfält med nyckeln som namnet på underfältet och typen som typen av fältvärdet. Något sådant här:
interface Inputs{
emails: { email: string, active: boolean, i: number }[]
}
const obj:Inputs = {emails: [ {email: "...", active: true, i: 100} ]}
Gränssnittet som tar emot detta som en generisk typ har en "name"-egenskap som kommer att ta emot (keyof) namnet på underfältet (t.ex. active) och en funktion med en parameter som måste ta emot typen av underfältet som definieras i name-egenskapen.
Något liknande detta
[
{
name: "active",
component: ({ value, values }) => {
console.log(value, values);
return <>Component</>;
}
}
]
I det här exemplet måste värdet ha "boolean" som enda accepterade typ eftersom den aktiva nyckeln i objektet har en boolean-typ.
Jag lyckades få nästan allt jag ville göra. Det enda problemet är att istället för att få exakt typen av underfältet får parametern i funktionen en union av alla typer av objektet.
Så i det tidigare exemplet, eftersom "email" är av typen sträng, bör värdet vara av typen sträng, istället är den mottagande typen sträng | nummer | boolean (alla tillgängliga typer i objektet).
Jag vet inte om jag har kunnat förklara mig väl, men jag har förberett en sandlåda för att göra det bättre
https://codesandbox.io/s/boring-fast-pmmhxx?file=/src/App.tsx
interface Options<
T extends { [key: string]: unknown }[],
Key extends keyof T[number]
> {
values: T;
value: Key;
}
interface InputDef<
T extends { [key: string]: any }[],
Key extends keyof T[number]
> {
name: Key;
component: (props: Options<T, T[number][Key]>) => React.ReactNode;
}
interface Props<T extends { [key: string]: [] }, Key extends keyof T> {
name: Key;
inputs: InputDef<T[Key], keyof T[Key][number]>[];
callback: (values: T) => void;
}
interface Inputs {
firstName: string;
lastName: string;
emails: { email: string; active: boolean; other: number }[];
}
const GenComponent = <T extends { [key: string]: any }, Key extends keyof T>({
name,
inputs
}: Props<T, Key>) => {
console.log(inputs);
return (
<div>
{name} {JSON.stringify(inputs)}
</div>
);
};
interface MainComponentProps {
callback: TestCallback<Inputs>;
}
const MainComponent: React.FC<MainComponentProps> = ({ callback }) => {
return (
<>
<GenComponent
callback={callback}
name="emails"
inputs={[
{
name: "active",
component: ({ value, values }) => {
console.log(value, values);
return <>Component</>;
}
}
]}
/>
</>
);
};
type TestCallback<Data> = (values: Data) => void;
function test<Data>(values: Data): void {
console.log(values);
}
export default function App() {
return (
<div className="App">
<MainComponent callback={test} />
</div>
);
}
På rad 57, eftersom namnet i objektet är "active" bör värdets typ vara "boolean" istället för "string | number | boolean". Hur kan jag uppnå detta?
Tack!
Svar #1
Jag kommer att förenkla ditt exempel för att visa var problemet ligger och hur man åtgärdar det. Först har du en generisk KeyValFunc<T, K>-typ som tar en objekttyp T och en av dess nyckeltyper K, och innehåller både den nyckeln och en funktion som accepterar ett värde vars typ matchar objekttypens egenskap för den nyckeln:
interface KeyValFunc<T, K extends keyof T> {
key: K,
valFunc: (val: T[K]) => void
}
Så för detta gränssnitt
interface Foo {
x: number,
y: string,
z: boolean
}
Du kan skriva ett värde av typen KeyValFunc<Foo, "x"> vars key är av typen "x" och vars valFunc är av typen (val: number) => void:
const obj: KeyValFunc<Foo, "x"> = { key: "x", valFunc: val => val.toFixed() }; // okay
Det är allt bra och väl, men nu vill du ha en array av KeyValFunc<T, K> för någon given T men där du inte bryr dig om vilket specifikt K är, och faktiskt kan K vara olika för varje array-element. Det vill säga, du vill ha en heterogen array-typ. Din idé är att skriva det som KeyValFunc<T, keyof T>[]:
type KeyValFuncArray<T> = KeyValFunc<T, keyof T>[];
Men tyvärr fungerar detta inte:
const arr: KeyValFuncArray<Foo> = [
{ key: "x", valFunc: val => val.toFixed() } // error!
// ---------------------------> ~~~~~~~
// Property 'toFixed' does not exist on type 'string | number | boolean'.
]
Varför inser inte kompilatorn att x-tangenten hör ihop med number-värdet? Varför är val typad som string | number | boolean?
Problemet är att KeyValFunc<T, keyof T> inte är den elementtyp du vill att det ska vara. Låt oss undersöka det för Foo:
type Test = KeyValFunc<Foo, keyof Foo>;
/* type Test = KeyValFunc<Foo, keyof Foo> */;
Åh, det är inte särskilt informativt. Låt oss definiera en identitetssatt typ så att vi kan använda den på KeyValFunc för att se varje egenskap.
type Id<T> = { [K in keyof T]: T[K] };
type Test = Id<KeyValFunc<Foo, keyof Foo>>;
/* type Test = {
key: keyof Foo;
valFunc: (val: string | number | boolean) => void;
} */
Så det är problemet. Genom att använda keyof T för K kan varje element i arrayen ha vilken egenskapsnyckel som helst av Foo (keyof Foo), och parametern till valFunc kan vara vilket egenskapsvärde som helst av Foo (Foo[keyof Foo]). Det är inte vad vi vill.
Istället för att plugga in keyof T som K i KeyValFunc<T, K>, vill vi egentligen distribuera KeyValFunc<T, K> över unioner i K. Det vill säga, för varje K i keyof T-unionen vill vi utvärdera KeyValFunc<T, K> och sedan kombinera dem tillsammans i en ny union.
Här är ett sätt att skriva det på:
type SomeKeyValueFunc<T> = { [K in keyof T]-?: KeyValFunc<T, K> }[keyof T]
Detta är en mappad typ som omedelbart indexeras med alla dess nycklar, för att producera den unionsegenskap vi vill ha. Låt oss verifiera att den gör det vi vill för Foo:
type Test = SomeKeyValueFunc<Foo>;
//type Test = KeyValFunc<Foo, "x"> | KeyValFunc<Foo, "y"> | KeyValFunc<Foo, "z">
Ja, det är bättre. Nu är Test själv en union av tre typer, var och en av dem är KeyValFunc för en viss nyckel i Foo. Och för KeyValFuncArray<T> vill vi använda SomeKeyValueFunc<T> istället för KeyValueFunc<T, keyof T>:
type KeyValFuncArray<T> = SomeKeyValueFunc<T>[];
Och plötsligt fungerar saker som förväntat:
const arr: KeyValFuncArray<Foo> = [
{ key: "x", valFunc: val => val.toFixed() } // okay
]
Lekplatslänk till kod
