Typescript: Sende en unionstype til en overlasta funksjon?


Jeg har denne klassen:

type Params = {
    data: string | string[]
}

class Test {
    data: string | string[]

    constructor(params: Params) {
        this.data = this.initData(params.data)
    }

    initData(data: string): string
    initData(data: string[]): string[]
    initData(data: string | string[]): string | string[] {
        if (typeof data === 'string') {
            return data as string
        } else {
            return data as string[]
        }
    }
}

Men jeg får denne feilen:

No overload matches this call.
  Overload 1 of 2, '(data: string): string', gave the following error.
    Argument of type 'string | string[]' is not assignable to parameter of type 'string'.
      Type 'string[]' is not assignable to type 'string'.
  Overload 2 of 2, '(data: string[]): string[]', gave the following error.
    Argument of type 'string | string[]' is not assignable to parameter of type 'string[]'.
      Type 'string' is not assignable to type 'string[]'.ts(2769)

Untitled-1(38, 5): The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.

Jeg fant en dum løsning som hjelper:

constructor(params: Params) {
    this.data = this.initData(params.data)

    if (typeof params.data === 'string') {
        this.data = this.initData(params.data)
    }
    else {
        this.data = this.initData(params.data)
    }
}

Og jeg skjønner på en måte hvorfor dette fungerer, men kanskje det finnes en bedre måte å løse dette på?

Svar #1

Det finnes en langvarig åpen funksjonsforespørsel på microsoft/TypeScript#14107 for å støtte å kalle en overbelastet funksjon med en passende union av dens argumenter. Det vil si, å distribuere overbelastningsresolusjon over unjoner av argumenttyper. Foreløpig er det ikke en del av språket, og det finnes bare arbeidsomveier.

Hvis du vil kalle en overbelastet funksjon i dag, må du sørge for at kallet er passende for minst én av dens kallesignaturer. Den vanligste løsningen er derfor å legge til den manglende kallesignaturen som tilsvarer ditt mislykkede kall:

class Test {
  data: string | string[]

  constructor(params: Params) {
    this.data = this.initData(params.data)
  }

  // call signatures
  initData(data: string): string
  initData(data: string[]): string[]
  initData(data: string | string[]): string | string[]; // <-- add this

  // implementation
  initData(data: string | string[]): string | string[] {
    return data
  }
}

Det er ganske enkelt å gjøre her fordi du bare trenger å legge til én ekstra anropssignatur. Men hvis du hadde 𝒏-anropssignaturer fra starten, ville en kombinasjon av alle mulige samlinger av dem resultere i noe i størrelsesorden 2𝒏 anropssignaturer. Den vokser veldig raskt med 𝒏. For å unngå eksponentiell vekst, kan du bare ha en enkelt generisk anropssignatur som bruker en distribuerbar betinget type for å beregne returtypen:

class Test {
  data: string | string[]

  constructor(params: Params) {
    this.data = this.initData(params.data)
  }

  // call signature
  initData<T extends string | string[]>(data: T): 
    T extends string ? string :
    T extends string[] ? string[] :
    never;

  // implementation
  initData(data: string | string[]): string | string[] {
    return data
  }
}

Nå kan den enkeltstående kallsignaturen automatisk distribuere over foreninger, og den skalerer lineært med antall opprinnelige kallsignaturer. Den er fortsatt implementert som en overbelastning, fordi kompilatoren for øyeblikket ikke er i stand til å verifisere kroppene til funksjoner som returnerer generiske betingede typer. Se microsoft/TypeScript#33912 for en diskusjon om det. Så vi bruker "løsningen" i implementeringen av overbelastede funksjoner for å unngå typepåstander her.

Lenke til kode på lekeplassen

Relaterte artikler