Typescript kräver parameter om en annan parameter skickas, flera gånger


Jag försöker göra en distribuerad unionstyp där överlämnandet av en nyckel gör andra nycklar obligatoriska.

interface BaseArgs {
    title: string
}

interface FuncPagerArgs {
    enablePager: true
    limit: number
    count: number
}

type FuncArgs = (FuncPagerArgs & BaseArgs) | BaseArgs;

function func(args: FuncArgs) {
    if ("enablePager" in args) {
        pager({ limit: args.limit, count: args.count });
    }
}

interface PagerArgs {
    limit: number
    count: number
}

function pager(args: PagerArgs) {

}

func({
    title: "something",
    enablePager: true
})

https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=31&pc=3#code/JYOwLgpgTgZghgYwgAgEJwM4QIJQOYbIDeAUMucmMGADYQBcyGYUoeJAviSaJLIigBiAVxAIACnDzRcBYmQoQQcAEZ1J0qIxbCIC8jWABbaoxDCjK6PuQIA9qLBmLVqJ25gAngAchohLKEALzIABQiYhoy+IQAZGiYODEAlMgAPglYgQDc3DD+VHYgyPlioXAxjBEBKfIUyMAwYQBESqrqUtDNDcUVBKmk9fXenVChRMiGJk7IfRgAdFPUADS2DuCMc-P2jsgcybn1XFw84NDwSMhRUIF1FEsz5pbW9TsbyE+u7iSlCIXFI005UqV1GgQG3BOv3GNiotAYyGaGDsRggYAAFmxmssbG01BBrtooLpOMkgA

För mig borde inte den koden gå igenom validering eftersom jag anropar func samtidigt som jag skickar med enablePager, men jag skickade inte med limit eller count när båda krävs när enablePager är true. I mitt verkliga exempel försökte jag använda detta mönster för 3 eller 4 olika funktionsbooleaner, där varje skulle förändra kontraktet för att kräva fler fält. Tyvärr kan jag inte ens få den första funktionsbooleans att fungera, än mindre flera.

Svar #1

Problemet vi ser här är relaterat till att inte ha korrekt diskriminant mellan elementen i unionen. Vi har överlappning mellan två medlemmar (FuncPagerArgs & BaseArgs) | BaseArgs, överlappningen är lika med BaseArgs. Vad som är mycket viktigt är också beteendet hos funktionsargumenttyper i TS:

När man jämför typer av funktionsparametrar lyckas tilldelningen om antingen källparametern kan tilldelas målparametern, eller tvärtom.

Vad betyder det - om vår typ kan tilldelas den önskade typen, eller den önskade typen kan tilldelas vår argumenttyp kan du skicka den. Tänk på testet:

type ArgType = {
    title: "something",
    enablePager: true
}
type IsAssignable = ArgType extends FuncArgs ? true : false; // true, can be passed

Så typen du skickar är tilldelningsbar till den önskade typen, då tillåter TS att den skickas. Fråga mig inte varför, detta är designbeslut, och du kan förstå det som ett kompromiss, att vara striktare skulle blockera många önskade beteenden, åtminstone är det vad som anges i TS-dokumentationen jag har länkat ovan. För säkerhets skull finns det nu problemet inne i koden som kommer att orsaka ett runtime-fel.

För att åtgärda det behöver vi skapa en korrekt diskriminerad union utan överlappning.

interface BaseArgs {
    title: string
}

interface OnlyTitle extends BaseArgs {
    kind: 'TITLE'
}

interface FuncPagerArgs extends BaseArgs {
    kind: 'PAGER'
    enablePager: true
    limit: number
    count: number
}

type FuncArgs = OnlyTitle | FuncPagerArgs;

function func(args: FuncArgs) {
    if (args.kind === 'PAGER') {
        pager({ limit: args.limit, count: args.count });
    }
}

interface PagerArgs {
    limit: number
    count: number
}

function pager(args: PagerArgs) {

}

func({
    kind: "TITLE",
    enablePager: true
}) // error 

func({
    kind: "TITLE",
    title: 'title'
}) // correct 

Det jag gjorde är diskriminant i form av kind-egenskapen som kommer att tillåta på rätt medlemmar utan någon överlappning. Det betyder att du nu bara kan använda antingen den ena eller den andra varianten, typen överlappar aldrig.

Redigera efter kommentaren

I kommentaren har författaren uppgett att han behöver kombinera alla alternativ tillsammans, men när något är aktiverat bör alla alternativ som är relaterade till det inkluderas. Denna typ av struktur kan vi göra genom att separera alternativ i grupper + sammanfoga dem. Tänk på:

interface Base {
    title: string
}
interface Pager  {
  pager?: {
    limit: number
    count: number
  }  
}

interface Sort {
  sorter?: {
    columns: string[] // example property
  }  
}

type Options = Pager & Sort & Base;

function func(args: Options) {
    if (args.pager) {
        pager({ limit: args.pager.limit, count: args.pager.count });
    }
    if (args.sorter) {
       sorter(args.sorter.columns)
    }
}

func({
  title: 'title',
  pager: {count: 2, limit: 10} // all fields its ok
})


func({
  title: 'title',
  pager: {count: 2} // error limit is missing
})

Nu kan vi ha pager och sorter tillsammans, när du har pager behöver du ange alla alternativ som är relaterade till det eftersom de krävs.