Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript workaround to return narrowed types

Tags:

typescript

In typescipt, I need a way for a function to, given an argument of a particular type, return an object of a type that is related to the type of the argument.

See below for an example. I need a way to make 'const response = ...' more narrowly typed than it is.

The example below is for having requests of particular types linked to responses that are relevant only to the requests given. For example, given a request that finds information about a user, we want to have a the response that contains their name and age. But when given a request that finds information about a car, we want to have a response with information about the make and the miles of the car. We will only ever want to use the 'user' response for the 'user' request and similar for 'car'.

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)
    }
}

UPDATE : Solution 1 Inspired by Variable return types based on string literal type argument

One approach is to overload the 'performRequest' signature like so:

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)
    }
}

However, I would really like for the library that maintains the request and response types to not have to alter the signature of the function in the application that is using the request and response types (performRequest). So I'd still like to hear other solutions.

UPDATE Solution 2 Thanks for Gerrit Birkeland from TS Gitter channel for this:

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()
    }
}
like image 311
Rene Wooller Avatar asked Jan 10 '18 01:01

Rene Wooller


People also ask

How do I return different data types in TypeScript?

Returning the type value from a function is pretty simple. All you need to do is add a : between the closing parenthesis of the signature method ,and the opening curly bracket. After the colon, write the data type the function will return. This may either be a string, number, boolean, void, or and many more.

How do you handle unknown types in TypeScript?

unknown is the type-safe counterpart of any . Anything is assignable to unknown , but unknown isn't assignable to anything but itself and any without a type assertion or a control flow based narrowing. Likewise, no operations are permitted on an unknown without first asserting or narrowing to a more specific type.

How do you type narrow union TypeScript?

TypeScript Union Type Narrowing To narrow a variable to a specific type, implement a type guard. Use the typeof operator with the variable name and compare it with the type you expect for the variable.

Can a function return a type TypeScript?

We can return any type of value from the function or nothing at all from the function in TypeScript. Some of the return types is a string, number, object or any, etc. If we do not return the expected value from the function, then we will have an error and exception.


1 Answers

You can (mostly) achieve the desired effect by adding a property to the RequestBase class. This property doesn't need to be used for anything, but it does need to exist.

(Copied from my gitter message)

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)
  }
}
like image 109
Gerrit0 Avatar answered Nov 15 '22 10:11

Gerrit0