user1234
user1234

Reputation: 9028

How to narrow typescript array of string literal/string enum values

Suppose I have an interface that looks like:

type RequestProps = "a" | "b" | "c"
interface Request {
   props: RequestProps[];
}

and some code that takes the request and returns a response object:

declare function sendRequest(r: Request);

I would like to narrow r.props so that I can return a response with keys corresponding to the requested props. That is, if the request is made by a user like so:

const request = {
  props: ["a", "c"]
}
let response = sendRequest(request);

That response would autocomplete response.a and response.c as string values. Something along the lines of

type Response<T extends RequestProps[]> = { [key in T[number]]: string; }

I don't want the request maker to have to indicate string literals ("a" as "a"), or a tuple (["a", "c"] as ["a", "c"]), I would like sendRequest to be able to infer it or hint contextually or guard in some way that allows the type to be narrowed down to the actual literal values that the array contains, no matter how verbose (I will narrow down every permutation if I have to). Is this possible? String enums seem to have the same issue as string literals. Thanks!

Upvotes: 1

Views: 1756

Answers (1)

Matt McCutchen
Matt McCutchen

Reputation: 30929

Let's assume you define:

declare function sendRequest<T extends RequestProps[]>(r: {props: T}): Response<T>;

as I think you intended. Then the following works:

let response = sendRequest({
  props: ["a", "c"]
});

If you don't want to send the request right away, you have to somehow prevent the props elements from widening. A well-known trick is to run the request through a function that applies the appropriate contextual type:

declare function asRequest<T extends RequestProps[]>(r: {props: T}): {props: T};
let request = asRequest({
  props: ["a", "c"]
});

If you really really want to be able to write the request as an object literal and save it in a variable without using a helper function, you can use a bunch of single-member enums, though that seems unreasonable to me:

enum AA { A = "a" }
enum BB { B = "b" }
enum CC { C = "c" }
const A = AA.A, B = BB.B, C = CC.C;
type RequestProps2 = AA | BB | CC;
interface Request2 {
   props: RequestProps2[];
}

type Response2<T extends RequestProps2[]> = { [key in T[number]]: string; }
declare function sendRequest2<T extends RequestProps2[]>(r: {props: T}): Response2<T>;

let request2 = {
    props: [A, C]
};
let response2 = sendRequest2(request2);

Upvotes: 1

Related Questions