问题
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
回答1:
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.
回答2:
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.
回答3:
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/);
});
});
来源:https://stackoverflow.com/questions/43677527/typescript-type-ignore-case