Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to check if string is included in string literal type without using a type assertion?

Tags:

typescript

I often have string literal types like this one:

const supportedLanguageTags = [
    "de-at",
    "de-ch",
    "de-de",
    "en-gb",
    "en-us",
] as const;

type LanguageTag = typeof supportedLanguageTags[number];

function isSupportedLanguageTag(tag: string): tag is LanguageTag {
    return (supportedLanguageTags as unknown as string[]).includes(tag);
}

Here I use an array to define the type but also to be able to check if a random string is included in the literal type.

However I really don't like the type assertion. Do you have suggestions to get rid of it?

One solution would be to use an object instead of an array like this:

const supportedLanguageTags = {
    "de-at": 1,
    "de-ch": 1,
    "de-de": 1,
    "en-gb": 1,
    "en-us": 1,
};

type LanguageTag = keyof typeof supportedLanguageTags;

function isSupportedLanguageTag(tag: string): tag is LanguageTag {
    return tag in supportedLanguageTags;
}

But here I don't like having to define a random value for every object property.

like image 286
Krisztián Balla Avatar asked Oct 28 '25 18:10

Krisztián Balla


2 Answers

As mentioned in the answer to a similar question, the TypeScript typings for the Array.prototype.includes() call signature requires that the searched value be of the same type as the array elements. If the array elements are of type string, then you can search for any string. But if the array elements are of some narrower type than string, such as a union of string literal types, then you will be prevented from searching for an arbitrary string. This restriction is not really necessary for type safety; you should safely be able to search for something wider than the element types.

The issue microsoft/TypeScript#26255 was opened to ask for support here, but it was closed as a duplicate of microsoft/TypeScript#14520 which is about a way to represent supertype constraints. TypeScript only has extends, where U extends T means that U must be some subtype of T. For Array<T> you really want something like includes<U super T>(searchElement: U): boolean, where U super T means that U must be some supertype of T. TypeScript doesn't have such syntax, though. You can simulate it with conditional types, like includes<U extends (T extends U ? unknown : never)>(searchElement: U), but that's fairly complicated and is not part of the TypeScript typings for now.


One thing you can do here is to safely widen the type of your array to readonly string[]. A readonly array type is a supertype of string not known to have the mutating methods like push(). An array of string literals can be safely widened to readonly string[] since you won't be modifying its contents, and it's always safer to read a value of any string literal type into something that expects a string.

Type assertions allow both (safe) widening and (unsafe) narrowing. But if you want to make sure that you haven't accidentally done an unsafe narrowing, you can forgo type assertions and instead annotate a new variable. Type annotations only support safe widenings.

That means you can write this:

function isSupportedLanguageTag(tag: string): tag is LanguageTag {
  const s: readonly string[] = supportedLanguageTags; // okay
  return s.includes(tag); // okay
}

And you have type safety. Again, the s variable isn't really necessary, and a type assertion will reduce this to the terser return (supportedLanguageTage as readonly string[]).includes(tag); if you value concision over safety guarantees.

Playground link to code

like image 108
jcalz Avatar answered Oct 31 '25 09:10

jcalz


There is a known issue with Array.prototype.includes but I can't find the link. In order to trick TypeScript you can overload your function. Since overloads works biveriantly TS will allow you use tag is LanguageTag return type:

const supportedLanguageTags = [
  "de-at",
  "de-ch",
  "de-de",
  "en-gb",
  "en-us",
] as const;

type LanguageTag = typeof supportedLanguageTags[number];

function isSupported(tag: string): tag is LanguageTag
function isSupported(tag: any) {
  return supportedLanguageTags.includes(tag)
}


isSupported('hello') // ok
isSupported(42) // error

Playground

Yes, I have used any, but you are not allowed to call isSupported with number - only string.

like image 39
captain-yossarian Avatar answered Oct 31 '25 08:10

captain-yossarian