Typescript-løsning for å returnere innsnevrede typer


I TypeScript trenger jeg en måte for en funksjon å, gitt et argument av en bestemt type, returnere et objekt av en type som er relatert til typen til argumentet.

Se nedenfor for et eksempel. Jeg trenger en måte å gjøre 'const response=...' mer spesifikt typet enn det er.

Eksemplet nedenfor er for å ha forespørsler av bestemte typer knyttet til svar som kun er relevante for de gitte forespørslene. For eksempel, gitt en forespørsel som finner informasjon om en bruker, ønsker vi å ha et svar som inneholder deres navn og alder. Men når vi får en forespørsel som finner informasjon om en bil, ønsker vi å ha et svar med informasjon om bilens merke og kjørelengde. Vi vil kun noen gang bruke 'bruker'-svaret for 'bruker'-forespørselen og tilsvarende for 'bil'.

class RequestBase {
}

class ResponseBase {
}

interface IFindUserReq {
    user_id :string
}
class FindUserRequest implements IFindUserReq {
    user_id :string
    constructor(user_id) {
        this.user_id = user_id
    }
}

interface IFindUserRes {
    name :string
    age  :number
}
class FindUserResponse implements IFindUserRes {
    name :string
    age  :number
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

interface IFindCarReq {
    car_id :number
}
class FindCarRequest implements IFindCarReq {
    car_id :number 
    constructor(car_id) {
        this.car_id = car_id
    }
}

interface IFindCarRes {
    make :string
    miles :number
}
class FindCarResponse implements IFindCarRes {
    make :string
    miles  :number
    constructor(make, miles) {
        this.make = make;
        this.miles = miles;
    }
}

const request = new FindUserRequest("foo")
const response = performRequest(request) // the type here is 'RequestBase | undefined'. Is there any way to automatically narrow it to be FindCarResponse?

function performRequest(req :RequestBase) : RequestBase | undefined {
    if (req instanceof FindUserRequest) {
        return new FindUserResponse("foo", 23) // hard coded example for convenience
    } else if (req instanceof FindCarRequest) {
        return new FindCarResponse("toyota", 10000)
    }
}

OPPDATERING : Løsning 1 Inspirert av variable returtyper basert på streng-literal-typargument

En tilnærming er å overbelaste 'performRequest'-signaturen slik:

function performRequest(req :FindCarRequest) : FindCarResponse 
function performRequest(req :FindUserRequest) : FindUserResponse
function performRequest(req :RequestBase) : ResponseBase | undefined   {
    if (req instanceof FindUserRequest) {
        return new FindUserResponse("foo", 23) // hard coded example for convenience
    } else if (req instanceof FindCarRequest) {
        return new FindCarResponse("toyota", 10000)
    }
}

Jeg vil imidlertid virkelig at biblioteket som opprettholder forespørsels- og responstyper ikke skal måtte endre signaturen til funksjonen i applikasjonen som bruker forespørsels- og responstypene (performRequest). Så jeg vil fortsatt gjerne høre andre løsninger.

OPPDATERING Løsning 2 Takk til Gerrit Birkeland fra TS Gitter-kanalen for dette:

class RequestBase {
    _responseType : ResponseBase
}

class ResponseBase {
}

interface IFindUserReq {
    user_id :string
}
class FindUserRequest extends RequestBase implements IFindUserReq {
    _responseType :FindUserResponse
    user_id :string
    constructor(user_id) {
        super()
        this.user_id = user_id
    }
}

interface IFindUserRes {
    name :string
    age  :number
}
class FindUserResponse extends ResponseBase implements IFindUserRes {
    name :string
    age  :number
    constructor(name, age) {
        super()
        this.name = name;
        this.age = age;
    }
}

interface IFindCarReq {
    car_id :number
}
class FindCarRequest extends RequestBase implements IFindCarReq {
    _responseType :FindCarResponse
    car_id :number 
    constructor(car_id) {
        super()
        this.car_id = car_id
    }
}

interface IFindCarRes {
    make :string
    miles :number
}
class FindCarResponse extends ResponseBase implements IFindCarRes {
    make :string
    miles  :number
    constructor(make, miles) {
        super()
        this.make = make;
        this.miles = miles;
    }
}

const request = new FindUserRequest("foo")
const response = performRequest<FindUserRequest>(request) // the type of response here is ResponseBase, not sure why it's not narrowed 

function performRequest< T extends RequestBase>(req :T) :T["_responseType"]    {

    if (req instanceof FindUserRequest) {
        return new FindUserResponse("foo", 23) // hard coded example for convenience
    } else if (req instanceof FindCarRequest) {
        return new FindCarResponse("toyota", 10000)
    } else {
        return new ResponseBase()
    }
}

Svar #1

Du kan (for det meste) oppnå ønsket effekt ved å legge til en egenskap i RequestBase-klassen. Denne egenskapen trenger ikke å brukes til noe, men den må eksistere.

(Kopiert fra meldingen min på Gitter)

class RequestBase {
  _responseType: ResponseBase
}

class ResponseBase {}

class FindUserRequest implements RequestBase {
  _responseType: FindUserResponse
  user_id: string
  constructor(user_id: string) {
    this.user_id = user_id
  }
}

class FindUserResponse {
  name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const request = new FindUserRequest("foo")
const response = performRequest(request) // FindUserResponse | undefined

function performRequest<T extends RequestBase>(req: T): T["_responseType"] | undefined {
  if (req instanceof FindUserRequest) {
    return new FindUserResponse("foo", 23)
  }
}

Svar #2

Hvis du kan endre RequestBase til å være generisk, kan du legge til et generisk typeargument som representerer responstypen. Du kan deretter bruke dette til å returnere responstypen.

class RequestBase<TResponse> {
    performRequest() : TResponse {
        return null; // Dummy, could invoke the actual performRequest
    }
}
class FindCarRequest extends RequestBase<FindCarResponse> {
    constructor(public car_id: number) {
        super();
    }
}
class FindCarResponse {
    constructor(public make: string, public miles: number) {
    }
}

const request = new FindCarRequest(100)
const response = request.performRequest() // Will be typed as FindCarResponse

Merk Dessverre er det ikke et alternativ å beholde performRequest-funksjonen som en ekstern funksjon, da typeinferens ikke er smart nok til å hente den generiske typeparameteren fra basistypen:

function performRequest<T>(req: RequestBase<T>):T {
    return null; // Dummy
}

const request = new FindCarRequest(100)
const response = performRequest(request) // Will be typed as {} although I would have expected this to be FindCarResponse

const request2 = new RequestBase<FindCarResponse>()
const response2 = performRequest(request) // Will be typed as FindCarResponse

Relaterte artikler