Gjør en funksjonsparameter obligatorisk i TypeScript


I følgende kode:

type NoArg = {
  (): void
}

type OneArg = {
  (x:number): void
}

let noArg: NoArg = (x:number)=>{}
let oneArg: OneArg = ()=>{}

Bare den første tildelingen gir en kompilatorfeil. Jeg forstår hvorfor dette er tilfellet, fordi JavaScript tillater at funksjoner blir sendt med færre enn alle mulige argumenter, og det er forskjellig fra å si at argumentene er valgfrie, noe som har å gjøre med hvordan funksjonen kalles snarere enn hvordan den blir sendt. Se FAQ.

Men det sagt, finnes det noen måte å konstruere en versjon av OneArg-grensesnittet som ikke vil være kompatibel med en funksjon uten argumenter?

Jeg forstår at dette kan gjøres via merking eller nominell typing, for eksempel,

type OneArg = {
  (x:number): void
  _brand: “OneArg”
}

Men det, eller en hvilken som helst annen type nominal typeløsning, krever at man gjør ekstra arbeid ved tildeling (f.eks. må du eksplisitt legge til _brand-egenskapen til funksjonen).

Så mitt spørsmål er—er det noen måte å konstruere en type NoArg som vil mislykkes i den enkle tildelingen let oneArg: OneArg =()=>{}?

FAQ-en lenket ovenfor sier «Det finnes for øyeblikket ingen måte i TypeScript å angi at en callback-parameter må være til stede.» Betyr det at det helt utelukker det jeg ønsker å gjøre her? Jeg håper det ikke gjør det, siden dette ikke er en callback-parameter, men kanskje begrunnelsen er den samme.

OPPDATERING: I kommentarene nedenfor ble det stilt et spørsmål om hvorvidt dette kunne oppnås med en typebeskyttelsestest. Svaret, så vidt jeg kan se, er nei, fordi typeoverlappet betyr at typebeskyttelsen ikke vil snevre inn typene. Du kan se det i denne lekeplassen.

Svar #1

Etter litt diskusjon med @aluan-haddad, har jeg en delvis løsning. Jeg kan ikke få nøyaktig det jeg ba om, dvs. en "automatisk" typedefinisjon som skiller mellom en funksjon med påkrevd parameter og en uten, men jeg har kommet opp med en "diskriminator"-funksjon som vil skille mellom de to funksjonene og anvende en passende merket type.

Løsningen baserer seg på å bruke en betinget type, kombinert med det faktum at funksjonen med ett argument er inkompatibel med typen uten argument (selv om det motsatte ikke er tilfelle), for å tvinge frem de riktige typene.

type NoArg = {
    _brand: 'NoArg'
    ():void
}

type OneArg = {
  _brand: 'OneArg'
  (x:number): void
}

// Correct typings
let noArg: NoArg = discriminator(()=>{})
let oneArg: OneArg = discriminator((x:number)=>{})

// Both of these error, as hoped!
let noArgError: NoArg = discriminator((x:number)=>{})
let oneArgError: OneArg = discriminator(()=>{})

function discriminator<T extends (x:any)=>any>(myFunc: T) {
    let discriminatedFunc
    if(myFunc.length === 0) {
        discriminatedFunc = {
            _brand: 'NoArg',
            myFunc
        }
    } 
    else {
        discriminatedFunc = {
            _brand: 'OneArg',
            myFunc
        }
    }
    return discriminatedFunc as unknown as T extends ()=>any ? NoArg : OneArg
}

Og på en lekeplass.

NB: Til poenget mitt om at dette avhenger av at en funksjon med ett argument er inkompatibel med funksjon med null type, merk at den omvendte versjonen av den betingede typen T extends (x:number)=>any ? OneArg : NoArg ikke vil fungere, fordi den alltid løses til OneArg.

Er dette nå bedre enn å «brande» funksjonene i første omgang via konstruktørfunksjoner? Jeg tror det, fordi du bare trenger én diskrimineringsfunksjon kontra to branding-funksjoner. Og også fordi du ikke nødvendigvis trenger å kjenne signaturene til funksjonene dine på forhånd, altså når du sender dem til diskriminatoren.

Noen som ser noen problemer med denne løsningen? Finnes det en bedre, eller er dette alt TS tillater?

Relaterte artikler