Restrictions on number using typescript

Viewed 83

I'm trying to define a type/interface (don't care which for the purpose of this exercise) that uses generics that has two properties:

interface Response<T> {
  status: number;
  data: T | undefined;
}

The restriction I want to capture is that when status !== 200, data must be undefined. When status === 200, data must be T. This way, I don't always have to check to see if response.data is not undefined after I check to make sure response.status is 200:

if (response.status === 200 && response.data) {
  // Wouldn't it be nice it TS just knew that response.data is
  // not undefined without having to check it explicitly?
}

So far, I have this:

interface IOkResponse<T> {
  status: 200;
  data: T;
}

interface IErrorResponse {
  status: number;
  data: undefined;
}

type Response<T> = IOkResponse<T> | IErrorResponse;

Of course, since IErrorResponse does not restrict status to numbers that are not 200, this doesn't work. How would I go about adding that restriction?

3 Answers

I agree with @jcalz, there is no hacky way to do it. One thing you cand do is typeguards. It will provide to your code type safety but you should pay for it - function overhead.


interface IOkResponse<T> {
  status: 200;
  data: T;
}

interface IErrorResponse {
  status: number;
  data: undefined
}

type Result<T> = IOkResponse<T> | IErrorResponse;

const isOK = <T,>(arg: Result<T>): arg is IOkResponse<T> => arg.status === 200

const foo = <T,>(arg: Result<T>) => {
  if (isOK(arg)) {
    const y = arg //  IOkResponse<T> 
  } else {
    const z = arg // IErrorResponse;
  }
}
class Response<T> {
    status: number = null;
    get data(): T {
        return this.status === 200 && this.data || undefined;
    }
}

How about trying something like this?, data will be undefined if the status === 200

While @captain-yossarin has a functional solution that's more generic in nature (which is why I formally accepted that as the answer), here's the solution I went with...

export enum StatusCode {
  OK = 200,
  NoContent = 204,
  NotModified = 304,
  BadRequest = 400,
  Unauthorized = 401,
  // Other status codes that the client cares about go here
}

interface IOkResponse<T> {
  statusCode: StatusCode.OK;
  data: T;
}

interface IErrorResponse {
  statusCode: Exclude<StatusCode, StatusCode.OK>;
  data: undefined;
}

export type ApiResponse<T> = IOkResponse<T> | IErrorResponse;

This function, which is responsible for creating an instance of my ApiResponse<T> object works fine because TypeScript will implicitly cast number to StatusCode.

async function parseResponse<T>(response: Response): Promise<ApiResponse<T>> {
  const text = await response.text();
  const statusCode = response.status;
  if (statusCode === StatusCode.OK) {
    const data = JSON.parse(text) as T;
    return { statusCode, data };
  }
  return { statusCode, data: undefined };
}

Now, I can use my ApiResponse<T> object without unnecessary undefined checks when statusCode === 200.

const ar1: ApiResponse<Test> = { statusCode: 200, data: { value: 5 } };
if (ar1.statusCode === StatusCode.OK) {
  console.log(ar1.data.value);
}
Related