typescript - Hur gör man så att en objektsegenskap beror på en annan i en generisk typ?


Jag försöker skapa ett objekt med en egenskap som beror på en annan egenskap.

Detta är ett mycket förenklat exempel på vad jag har försökt hittills.
Jag förväntade mig att T skulle härledas från name. value bör då begränsas till det giltiga värdet i TypeA.

type TypeA = {
  some: 'some2';
  thing: 'thing2';
};

type TypeAUnion = keyof TypeA;

type TestType<T extends TypeAUnion = TypeAUnion> = {
  name: T;
  value: TypeA[T];
};

const test1: TestType = {
  name: 'some',
  value: 'some2',
};

const test2: TestType = {
  name: 'some',
  value: 'thing2', // shouldn't be allowed here
};

REDIGERA :

Ett bättre exempel på vad jag försöker göra.

type StateType = {
  thingA: string;
  thingB: number;
};

type StateKeysUnion = keyof StateType;

const state: StateType = {
  thingA: 'somestring',
  thingB: 10,
};

type PayloadType<T extends StateKeysUnion = StateKeysUnion> = {
  key: T;
  value: StateType[T];
};

const setThing = (payload: PayloadType) => {
  state[payload.key] = payload.value;
};

setThing({
  key: 'thingA',
  // expected to only accept string
  value: true,
});


setThing({
  key: 'thingB',
  // expected to only accept number
  value: 'asdas',
});

Svar #1

Det verkar som att du vill ta din ursprungliga StateType-typ

type StateType = {
    thingA: string;
    thingB: number;
};

och gör PayloadType till en union av key/value-par för varje egenskap i keyof StateType:

type PayloadType = {
    key: "thingA";
    value: string;
} | {
    key: "thingB";
    value: number;
}    

Detta kommer bara låta dig tilldela de korrekta värdena och kommer att klaga om du matchar fel key och value:

let payloadType: PayloadType;
payloadType = { key: "thingA", value: "abc" }; // okay
payloadType = { key: "thingA", value: 123 }; // error, number is not string
payloadType = { key: "thingB", value: 123 }; // okay

Du kan definiera PayloadType programmatiskt genom att skriva det som en distributiv objekttyp, vilket är en mappad typ över varje egenskap, i vilken vi omedelbart indexerar med föreningen av nycklar för att få föreningen av objekttyper som vi bryr oss om:

type PayloadType = { [P in keyof StateType]: {
    key: P;
    value: StateType[P];
} }[keyof StateType];

Ditt ytterligare exempel ser ut så här:

const state: StateType = {
    thingA: 'somestring',
    thingB: 10,
};

const setThing = (payload: PayloadType) => {
    state[payload.key] = payload.value; // error!
    //^^^^^^^^^^^^^^^^ <-- string | number not assignable to never
};

Även med PayloadType korrekt, klagar kompilatorn på state[payload.key]=payload.value. Höger sida av tilldelningen är string | number, men vänster sida måste vara string & number på grund av microsoft/TypeScript#30769. TypeScript har inte direkt stöd för korrelerade unioner, som beskrivs i microsoft/TypeScript#30581. Den ser payload.key som typen "thingA" | "thingB" och payload.value som typen string | number, och blir orolig för att du till exempel kan tilldela en string till state.thingA. Den kan inte se att detta är omöjligt.

Det rekommenderade sättet att hantera detta är att göra setThing() generisk på ett särskilt sätt som beskrivs i microsoft/TypeScript#47109.

Först gör vi PayloadType generisk igen, men fortfarande som en distribuerande objekttyp. Om du bara skriver PayloadType själv eller PayloadType<keyof StateType>, är det hela unionen. Men om du skriver PayloadType<K1> för någon viss egenskap K1 (som "thingA"), får du bara det motsvarande elementet i unionen:

type PayloadType<K extends keyof StateType = keyof StateType> = { [P in K]: {
    key: P;
    value: StateType[P];
} }[K];

Och nu fungerar setThing()-implementationen:

const setThing = <K extends keyof StateType>(payload: PayloadType<K>) => {
    state[payload.key] = payload.value; // okay
};

Det beror på att båda sidor av tilldelningen är av typen StateType[K].


Och låt oss verifiera att setThing() fungerar som önskat från den som anropars sida också:

setThing({ key: "thingA", value: "abc" }); // okay
// const setThing: <"thingA">(payload: { key: "thingA"; value: string; }) => void

setThing({ key: "thingA", value: 123 }); // error!
// ---------------------> ~~~~~

setThing({ key: "thingB", value: 123 }); // okay
// const setThing: <"thingB">(payload: { key: "thingB"; value: number; }) => void

Ser bra ut. Kompilatorn härleder K som att vara "thingA" eller "thingB" beroende på key-egenskapen, och kontrollerar sedan value mot den.

Lekplatslänk till kod

Svar #2

Det du försöker uppnå kan inte uttryckas med en generisk typ.

type Narrow<T extends string = string> = T;

// str type narrowing
type Out = Narrow<"hi">;
// type Out = "hi"

// default case
type Out2 = Narrow;
// type Out2 = string

// **values don't narrow the type**
let t: Narrow = "hi"
type Out3 = typeof t;
// type Out3 = string (default)

Om du skulle specificera some i det generiska fältet

const test2: TestType<"some"> = {
  name: 'some',
  value: 'thing2', // it now gives the error you're looking for.
};

Men eftersom du inte är det, antar T som standard keyof TypeA och låter dig blanda och matcha.

Du bör istället använda @jcalz unionsmetod. :)