TypeScript type ignore case

11,865

Solution 1

NEW ANSWER FOR TYPESCRIPT 4.1+

Welcome back! Now that TypeScript 4.1 has introduced template literal types and the Uppercase/Lowercase intrinsic string mapping types, we can now answer this question without needing regular expression types.


There are two main approaches. The "brute force" approach makes heavy use of recursive conditional types and unions to turn your xhrTypes into a concrete union of all possible ways of writing those strings where case doesn't matter:

type xhrTypes = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "CONNECT" | "HEAD";

type AnyCase<T extends string> =
    string extends T ? string :
    T extends `${infer F1}${infer F2}${infer R}` ? (
        `${Uppercase<F1> | Lowercase<F1>}${Uppercase<F2> | Lowercase<F2>}${AnyCase<R>}`
    ) :
    T extends `${infer F}${infer R}` ? `${Uppercase<F> | Lowercase<F>}${AnyCase<R>}` :
    ""


type AnyCaseXhrTypes = AnyCase<xhrTypes>;

If you inspect AnyCaseXhrTypes, you'll see that it is a 368-member union:

/* type AnyCaseXhrTypes = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | 
"CONNECT" | "HEAD" | "GEt" | "GeT" | "Get" | "gET" | "gEt" | "geT" | "get" | 
"POSt" | "POsT" | "POst" | "PoST" |  "PoSt" | "PosT" | "Post" | 
... 346 more ... | "head" */

You can then use this type in place of xhrType wherever you want case insensitivity:

function acceptAnyCaseXhrType(xhrType: AnyCaseXhrTypes) { }

acceptAnyCaseXhrType("get"); // okay
acceptAnyCaseXhrType("DeLeTe"); // okay
acceptAnyCaseXhrType("poot"); // error! "poot" not assignable to big union

The problem with the brute force approach is that it doesn't scale well with more or longer strings. Union types in TypeScript are limited to 100,000 members, and recursive conditional types only really go about 20 levels deep maximum before the compiler complains. So any moderately long words or moderately long list of words will make the above approach unfeasible.

type xhrTypes = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "CONNECT" | "HEAD"
 | "LONG STRINGS MAKE THE COMPILER UNHAPPY";

type AnyCaseXhrTypes = AnyCase<xhrTypes>; // error!
// Type instantiation is excessively deep and possibly infinite.
// Union type is too complex to represent

A way to deal with that is to switch away from using a specific concrete union, and instead switch to a generic type representation. If T is the type of a string value passed to acceptAnyCaseXhrType(), then all we want to do is make sure that Uppercase<T> is assignable to xhrType. This is more of a constraint than a type (although we can't use generic constraints directly to express this):

function acceptAnyCaseXhrTypeGeneric<T extends string>(
    xhrType: Uppercase<T> extends xhrTypes ? T : xhrTypes
) { }

acceptAnyCaseXhrTypeGeneric("get"); // okay
acceptAnyCaseXhrTypeGeneric("DeLeTe"); // okay
acceptAnyCaseXhrTypeGeneric("poot"); // error! "poot" not assignable to xhrTypes

This solution requires that you pull generic type parameters around in places you might otherwise not need them, but it does scale well.


So, there you go! All we had to do was wait for... (checks notes)... 3 years, and TypeScript delivered!

Playground link to code

Solution 2

Just so there's an answer on this post: No, it is not possible.

Update 5/15/2018: Still not possible. The closest thing, regex-validated string types, was not well-received the most recent time it was proposed at the language design meeting.

Solution 3

As @RyanCavanaugh said, TypeScript doesn't have case-insensitive string literals. [EDIT: I am reminded that there is an existing suggestion for TypeScript to support regexp-validated string literals, which would maybe allow for this, but it is not currently part of the language.]

The only workaround I can think of is to enumerate the most likely variants of those literals (say, all lowercase, init cap) and make a function that can translate between them if needed:

namespace XhrTypes {
  function m<T, K extends string, V extends string>(
    t: T, ks: K[], v: V
  ): T & Record<K | V, V> {
    (t as any)[v] = v;
    ks.forEach(k => (t as any)[k] = v);
    return t as any;
  }
  function id<T>(t: T): { [K in keyof T]: T[K] } {
    return t;
  }
  const mapping = id(m(m(m(m(m(m(m({},
    ["get", "Get"], "GET"), ["post", "Post"], "POST"),
    ["put", "Put"], "PUT"), ["delete", "Delete"], "DELETE"),
    ["options", "Options"], "OPTIONS"), ["connect", "Connect"], "CONNECT"),
    ["head", "Head"], "HEAD"));      

  export type Insensitive = keyof typeof mapping
  type ForwardMapping<I extends Insensitive> = typeof mapping[I];

  export type Sensitive = ForwardMapping<Insensitive>;     
  type ReverseMapping<S extends Sensitive> = 
    {[K in Insensitive]: ForwardMapping<K> extends S ? K : never}[Insensitive];

  export function toSensitive<K extends Insensitive>(
    k: K ): ForwardMapping<K> {
    return mapping[k];
  }

  export function matches<K extends Insensitive, L extends Insensitive>(
    k: K, l: L ): k is K & ReverseMapping<ForwardMapping<L>> {
    return toSensitive(k) === toSensitive(l);
  }
}

What ends up getting exported is the following types:

type XhrTypes.Sensitive = "GET" | "POST" | "PUT" | "DELETE" | 
  "OPTIONS" | "CONNECT" | "HEAD"

type XhrTypes.Insensitive = "get" | "Get" | "GET" | 
  "post" | "Post" | "POST" | "put" | "Put" | "PUT" | 
  "delete" | "Delete" | "DELETE" | "options" | "Options" |
  "OPTIONS" | "connect" | "Connect" | "CONNECT" | "head" | 
  "Head" | "HEAD"

and the functions

 function XhrTypes.toSensitive(k: XhrTypes.Insensitive): XhrTypes.Sensitive;

 function XhrTypes.matches(k: XhrTypes.Insensitive, l: XhrTypes.Insensitive): boolean;

I'm not sure what you (@Knu) need this for or how you plan to use it, but I'm imagining that you want to convert between sensitive/insensitive methods, or check to see if two case-insensitive methods are a match. Obviously you can do those at runtime by just converting to uppercase or doing a case-insensitive compare, but at compile time the above types may be useful.

Here's an example of using it:

interface HttpStuff {
  url: string,
  method: XhrTypes.Insensitive,
  body?: any
}
const httpStuff: HttpStuff = {
  url: "https://google.com",
  method: "get"
}

interface StrictHttpStuff {
  url: string,
  method: XhrTypes.Sensitive,
  body?: any
}
declare function needStrictHttpStuff(httpStuff: StrictHttpStuff): Promise<{}>;

needStrictHttpStuff(httpStuff); // error, bad method

needStrictHttpStuff({
   url: httpStuff.url, 
   method: XhrTypes.toSensitive(httpStuff.method) 
  }); // okay

In the above, there's a function that expects uppercase values, but you can safely pass it a case insensitive value if you use XhrTypes.toSensitive() first, and the compiler verifies that "get" is an acceptable variant of "GET" in this case.

Okay, hope that helps. Good luck.

Solution 4

While not the types that were asked for, if an enum would be okay then the following can be used for case-insensitive matching of enum string values:

/**
 * Gets an enumeration given a case-insensitive key. For a numeric enum this uses
 * its members' names; for a string enum this searches the specific string values.
 * Logs a warning if the letter case was ignored to find a match, and logs an error
 * including the supported values if no match was found.
 */
static toEnumIgnoreCase<T>(target: T, caseInsentiveKey: string): T[keyof T] {
    const needle = caseInsentiveKey.toLowerCase();

    // If the enum Object does not have a key "0", then assume a string enum
    const key = Object.keys(target)
      .find(k => (target['0'] ? k : target[k]).toLowerCase() === needle);

    if (!key) {
        const expected = Object.keys(target)
          .map(k => target['0'] ? k : target[k])
          .filter(k => isNaN(Number.parseInt(k)))
          .join(', ');
        console.error(`Could not map '${caseInsentiveKey}' to values ${expected}`);
        return undefined;
    }

    const name = target['0'] ? key : target[key];
    if (name !== caseInsentiveKey) {
        console.warn(`Ignored case to map ${caseInsentiveKey} to value ${name}`);
    }

    return target[key];
}

Of course, as this loops over possible values, it's really only meant to handle things like configuration files; all code should really use the enum values instead.

Some tests:

import Spy = jasmine.Spy;
import {ConfigHelper} from './config-helper';

// Should match on One, one, ONE and all:
enum NumberEnum { One, Two, Three }

// Should match on Uno, uno, UNO and all, but NOT on One, one, ONE and all:
enum StringEnum { One = 'Uno', Two = 'Dos', Three = 'Tres' }

describe('toEnumIgnoreCase', () => {

    beforeEach(function () {
        spyOn(console, 'warn');
        spyOn(console, 'error');
    });

    it('should find exact match for numeric enum', () => {
        const result = ConfigHelper.toEnumIgnoreCase(NumberEnum, 'One');
        expect(result).toBe(NumberEnum.One);
        expect(console.warn).not.toHaveBeenCalled();
        expect(console.error).not.toHaveBeenCalled();
    });
    it('should find case-insensitive match for numeric enum', () => {
        const result = ConfigHelper.toEnumIgnoreCase(NumberEnum, 'two');
        expect(result).toBe(NumberEnum.Two);
        expect(console.warn).toHaveBeenCalled();
        expect((console.warn as Spy).calls.mostRecent().args[0])
          .toMatch(/value Two/);
        expect(console.error).not.toHaveBeenCalled();
    });
    it('should yield undefined for non-match for numeric enum', () => {
        const result = ConfigHelper.toEnumIgnoreCase(NumberEnum, 'none');
        expect(result).toBe(undefined);
        expect(console.warn).not.toHaveBeenCalled();
        expect(console.error).toHaveBeenCalled();
        expect((console.error as Spy).calls.mostRecent().args[0])
          .toMatch(/values One, Two, Three/);
    });

    it('should find exact match for string enum', () => {
        const result = ConfigHelper.toEnumIgnoreCase(StringEnum, 'Uno');
        expect(result).toBe(StringEnum.One);
        expect(console.warn).not.toHaveBeenCalled();
        expect(console.error).not.toHaveBeenCalled();
    });
    it('should find case-insensitive match for string enum', () => {
        const result = ConfigHelper.toEnumIgnoreCase(StringEnum, 'dos');
        expect(result).toBe(StringEnum.Two);
        expect(console.warn).toHaveBeenCalled();
        expect((console.warn as Spy).calls.mostRecent().args[0])
          .toMatch(/value Dos/);
        expect(console.error).not.toHaveBeenCalled();
    });
    it('should yield undefined for name rather than string value', () => {
        const result = ConfigHelper.toEnumIgnoreCase(StringEnum, 'One');
        expect(result).toBe(undefined);
        expect(console.warn).not.toHaveBeenCalled();
        expect(console.error).toHaveBeenCalled();
        expect((console.error as Spy).calls.mostRecent().args[0])
          .toMatch(/values Uno, Dos, Tres/);
    });
    it('should yield undefined for non-match for string enum', () => {
        const result = ConfigHelper.toEnumIgnoreCase(StringEnum, 'none');
        expect(result).toBe(undefined);
        expect(console.warn).not.toHaveBeenCalled();
        expect(console.error).toHaveBeenCalled();
        expect((console.error as Spy).calls.mostRecent().args[0])
          .toMatch(/values Uno, Dos, Tres/);
    });
});
Share:
11,865

Related videos on Youtube

marco burrometo
Author by

marco burrometo

I am Marco Burrometo Born in '92. Based in northern Italy. My greatest passions are Technology and Music. I like to put user experience in first place and focus on customer's needs to archieve the best results. I have fun creating unique things and keeping myself updated on newest technologies. Bass guitar player since I was 15 🎸 Vinyl addicted 🎛📻 Proud dachshund owner and lover 🐶🦴

Updated on July 04, 2022

Comments

  • marco burrometo
    marco burrometo almost 2 years

    I have this type definition in TypeScript:

    export type xhrTypes = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "CONNECT" | "HEAD";
    

    Sadly, this is case sensitive...is there any way to define it case insensitive?

    thanks

    • Aluan Haddad
      Aluan Haddad about 7 years
      No that is not possible.
    • andreim
      andreim about 7 years
      No, but you can simulate enums.
    • marco burrometo
      marco burrometo about 7 years
      Okay, thanks! but i think I'll keep it case sensitive
  • Knu
    Knu almost 6 years
    Evidently if I am putting a bounty on this question it's not to get a brute force solution which requires to put every single possibilities—even something like pOsT—in a dictionary. Why can't I use the i regexp flag?
  • jcalz
    jcalz almost 6 years
    One may presume that if the current accepted answer written fairly recently by one of the maintainers of the language says it’s not possible then it is not possible. My answer is the only workaround I can conceive of. You can’t use a regexp because that is not part of the type system for string literals. There is an existing suggestion in GitHub to support this, but it’s not straightforward to add. Maybe you can help work on it if you’re interested in contributing to the development of the language!
  • jcalz
    jcalz almost 6 years
    @Knu what exactly is your use case? Maybe type guards along with branded types would help? Without a concrete use case I'm not sure if there's a better answer for you.
  • Knu
    Knu almost 6 years
    This makes no sense; it's such a basic use case, I can't believe it's not already supported. I didn't know he was one of the maintainers. I hope someone will come up with a clever hack.
  • Ozymandias
    Ozymandias almost 6 years
    What needs to change about the regex-validated string types proposal for it to be well-received?
  • jcalz
    jcalz over 3 years
  • marco burrometo
    marco burrometo over 3 years
    This is super! Maybe a little bit overkill for this specific case but the template literal types could be really powerful