Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript const assertions: how to use Array.prototype.includes?

I am trying to use an array of elements as union type, something that became easy with const assertions in TS 3.4, so I can do this:

const CAPITAL_LETTERS = ['A', 'B', 'C', ..., 'Z'] as const;
type CapitalLetter = typeof CAPITAL_LETTERS[string];

Now I want to test whether a string is a capital letter, but the following fails with "not assignable to parameter of type":

let str: string;
...
CAPITAL_LETTERS.includes(str);

Is there any better way to fix this rather than casting CAPITAL_LETTERS to unknown and then to Array<string>?

like image 796
dols3m Avatar asked Jun 12 '19 15:06

dols3m


2 Answers

The standard library signature for Array<T>.includes(u) assumes that the value to be checked is of the same or narrower type than the array's elements T. But in your case you are doing the opposite, checking against a value which is of a wider type. In fact, the only time you would say that Array<T>.includes<U>(x: U) is a mistake and must be prohibited is if there is no overlap between T and U (i.e., when T & U is never).

Now, if you're not going to be doing this sort of "opposite" use of includes() very often, and you want zero runtime efects, you should just widen CAPITAL_LETTERS to ReadonlyArray<string> via type assertion:

(CAPITAL_LETTERS as ReadonlyArray<string>).includes(str); // okay

If, on the other hand, you feel seriously enough that this use of includes() should be accepted with no type assertions, and you want it to happen in all of your code, you could merge in a custom declaration:

// global augmentation needed if your code is in a module
// if your code is not in a module, get rid of "declare global":
declare global { 
  interface ReadonlyArray<T> {
    includes<U>(x: U & ((T & U) extends never ? never : unknown)): boolean;
  }
}

That will make it so that an array (well, a readonly array, but that's what you have in this example) will allow any parameter for .includes() as long as there is some overlap between the array element type and the parameter type. Since string & CapitalLetter is not never, it will allow the call. It will still forbid CAPITAL_LETTERS.includes(123), though.

Okay, hope that helps; good luck!

like image 111
jcalz Avatar answered Nov 02 '22 11:11

jcalz


Another way to solve it is with a type guard

https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards

const myConstArray = ["foo", "bar", "baz"] as const

function myFunc(x: string) {
    //Argument of type 'string' is not assignable to parameter of type '"foo" | "bar" | "baz"'.
    if (myConstArray.includes(x)) {
        //Hey, a string could totally be one of those values! What gives, TS?
    }
}

//get the string union type
type TMyConstArrayValue = typeof myConstArray[number]

//Make a type guard
//Here the "x is TMyConstArrayValue" tells TS that if this fn returns true then x is of that type
function isInMyConstArray(x: string): x is TMyConstArrayValue {
    return myConstArray.includes(x as TMyConstArrayValue)

    //Note the cast here, we're doing something TS things is unsafe but being explicit about it
    //I like to this of type guards as saying to TS:
    //"I promise that if this fn returns true then the variable is of the following type"
}

function myFunc2(x: string) {
    if (isInMyConstArray(x)) {
        //x is now "foo" | "bar" | "baz" as originally intended!
    }
}

While you have to introduce another "unnecessary" function this ends up looking clean and working perfectly. In your case you would add

const CAPITAL_LETTERS = ['A', 'B', 'C', ..., 'Z'] as const;
type CapitalLetter = typeof CAPITAL_LETTERS[string];
function isCapitalLetter(x: string): x is CapitalLetter {
    return CAPITAL_LETTERS.includes(x as CapitalLetter)
}

let str: string;
isCapitalLetter(str) //Now you have your comparison

//Not any more verbose than writing .includes inline
if(isCapitalLetter(str)){
  //now str is of type CapitalLetter
}
like image 11
imagio Avatar answered Nov 02 '22 11:11

imagio