TypeScript类型忽略大小写

时间:2017-04-28 10:04:40

标签: typescript types case-insensitive typescript-typings

我在TypeScript中有这种类型定义:

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

可悲的是,这是区分大小写的...有没有办法将它定义为不区分大小写?

感谢

4 个答案:

答案 0 :(得分:7)

就这样在这篇文章上有一个答案:不,这是不可能的。

更新5/15/2018 :仍然无法做到。最接近的是,正则表达式验证的字符串类型,在语言设计会议上提出的最近时间并没有被广泛接受。

答案 1 :(得分:2)

TYPESCRIPT 4.1+的新答案

欢迎回来!现在TypeScript 4.1引入了template literal typesUppercase/Lowercase intrinsic string mapping types,我们现在可以回答这个问题而无需正则表达式类型。


有两种主要方法。 “强力”方法大量使用recursive conditional types和并集来将您的xhrTypes变成所有大小写无关紧要的所有可能写法的具体结合:

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>;

如果您检查AnyCaseXhrTypes,则会看到它是一个由368个成员组成的工会:

/* 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" */

然后,您可以在不区分大小写的任何地方使用此类型代替xhrType

function acceptAnyCaseXhrType(xhrType: AnyCaseXhrTypes) { }

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

暴力破解方法的问题在于,在使用更多或更长的字符串时缩放效果不佳。 TypeScript中的联合类型限制为100,000个成员,并且在编译器抱怨之前,递归条件类型实际上最多只能达到约20个深度。因此,任何中等长度的单词或中等长度的单词列表都将使上述方法不可行。

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

一种解决方法是放弃使用特定的具体联合,而改为使用generic type表示形式。如果T是传递给acceptAnyCaseXhrType()的字符串值的类型,那么我们要做的就是确保Uppercase<T>可分配给xhrType。这比类型更像constraint(尽管我们不能直接使用通用约束来表达这一点):

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

此解决方案要求您将通用类型参数放在原本可能不需要它们的地方,但是可以很好地扩展。


所以,您去了!我们要做的就是等待...(检查笔记)... 3年,TypeScript交付了!

Playground link to code

答案 2 :(得分:1)

正如@RyanCavanaugh所说,TypeScript没有不区分大小写的字符串文字。 [编辑:我注意到TypeScript存在suggestion以支持经过regexp验证的字符串文字,这可能允许这样做,但它目前不是该语言的一部分。]

我能想到的唯一解决方法是枚举这些文字的最可能的变体(例如,全部小写,初始化上限),并创建一个可以在需要时进行转换的函数:

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);
  }
}

最终导出的内容有以下几种:

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"

和功能

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

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

我不确定你(@Knu)需要这个或者你打算如何使用它,但我想你想要在敏感/不敏感的方法之间进行转换,或者检查一下是否有两个不区分大小写的方法方法是匹配的。显然,您可以通过转换为大写或进行不区分大小写的比较来在运行时执行这些操作,但在编译时,上述类型可能很有用。

以下是使用它的示例:

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

在上面,有一个函数需要大写值,但如果先使用XhrTypes.toSensitive(),则可以安全地传递一个不区分大小写的值,并且编译器会验证"get"是否为可接受的变体在这种情况下"GET"

好的,希望有所帮助。祝你好运。

答案 3 :(得分:0)

虽然不是所要求的类型,但是如果枚举可以,那么可以将以下内容用于区分大小写的枚举字符串值:

/**
 * 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];
}

当然,由于这会遍历所有可能的值,因此实际上只意味着处理配置文件之类的内容;所有代码实际上都应该使用enum值。

一些测试:

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/);
    });
});