【问题标题】:TypeScript type ignore caseTypeScript 类型忽略大小写
【发布时间】:2017-09-26 09:56:36
【问题描述】:

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

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

遗憾的是,这是区分大小写的……有没有办法定义它不区分大小写?

谢谢

【问题讨论】:

  • 不,这是不可能的。
  • 不,但你可以simulate enums
  • 好的,谢谢!但我想我会区分大小写

标签: typescript types case-insensitive typescript-typings


【解决方案1】:

所以这篇文章有一个答案:不,这是不可能的。

2018 年 5 月 15 日更新:仍然不可能。最接近的东西,正则表达式验证的字符串类型,最近一次在语言设计会议上提出时并没有受到好评。

【讨论】:

  • 对正则表达式验证的字符串类型提案需要进行哪些更改才能获得好评?
【解决方案2】:

正如@RyanCavanaugh 所说,TypeScript 没有不区分大小写的字符串文字。 [编辑:提醒我,TypeScript 有一个现有的suggestion 来支持正则表达式验证的字符串文字,这可能会允许这样做,但它目前不是语言的一部分。]

我能想到的唯一解决方法是枚举这些文字的最可能变体(例如,全小写,init cap)并创建一个可以在需要时在它们之间进行转换的函数:

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"的可接受变体这个案例。

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

【讨论】:

  • 显然,如果我在这个问题上悬赏,并不是要获得一个需要将每一种可能性——甚至像 pOsT 这样的东西——放入字典中的蛮力解决方案。为什么我不能使用i regexp 标志?
  • 人们可能会假设,如果该语言的一位维护者最近写的当前接受的答案说这是不可能的,那么它是不可能的。我的答案是我能想到的唯一解决方法。您不能使用正则表达式,因为它不是字符串文字类型系统的一部分。 GitHub 中有一个现有的suggestion 来支持这一点,但添加起来并不简单。如果您有兴趣为该语言的发展做出贡献,也许您可​​以提供帮助!
  • @Knu 您的用例到底是什么?也许type guardsbranded types 会有所帮助?如果没有具体的用例,我不确定是否有更好的答案。
  • 这毫无意义;这是一个基本的用例,我不敢相信它还没有得到支持。我不知道他是维护者之一。我希望有人能想出一个聪明的技巧。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

    TYPESCRIPT 4.1+ 的新答案

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


    有两种主要方法。 “蛮力”方法大量使用 recursive conditional types 和 union 将您的 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&lt;T&gt; 可分配给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

    【讨论】:

    • 这太棒了!对于这种特定情况,可能有点矫枉过正,但模板文字类型可能非常强大
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-08
    • 2021-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多