Typescript: DashCase and CamelCase intrinsic string types, or similar

Created on 22 Sep 2020  路  10Comments  路  Source: microsoft/TypeScript

Search Terms

dashcase snakecase

Suggestion

It would be great to also have DashCase and CamelCase and similar!

Use Cases

It is common in DOM libs to map from JS camelCase properties to DOM dash-case attributes (f.e. el.fooBar = 123 and <el foo-bar="123">), or CapitalizedClass names to dash-case custom element names (f.e. class FooBar extends HTMLElement and <foo-bar>).

Examples

type T1 = Dashcase<'fooBar' | 'BarBaz'> // 'foo-bar' | 'bar-baz'
type T2 = Camelcase<'foo-bar' | 'bar-baz'> // 'fooBar' | 'barBaz'

Or similar.

Checklist

My suggestion meets these guidelines:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
In Discussion Suggestion

Most helpful comment

Sorry for spamming, but just wanted to add an implementation of DashCase/ KebabCase as well. See this updated playground.

It's a bit more awkward as it eg. as to split on all A-Z uppercase chars as well:

type UpperCaseChars = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'X' | 'Y' | 'Z';

All 10 comments

Something that鈥檚 important to clarify in this suggestion is whether it鈥檚 important that these types be _intrinsic_ or if you just want some solution built into lib.d.ts. Various comments in #40336 demonstrate that these transformations are already possible, but are complex to write. If they were written into lib.d.ts, but were not intrinsics, would that be a good solution, or would there be performance / depth limit issues?

Type Camelcase could take an optional initialism union a la github.com/golang/lint but overall I think this will be a useful standard lib intrinsic.

If there's no consensus on a standard I don't know how typescript plans to regulate custom intrinsic implementations... would they be fishable like node_modules/@types? It would probably never end up in the standard lib but a jsonschema validator would be a favorite of mine.

Also I am on team KebabCase > DashCase

I don't know how typescript plans to regulate custom intrinsic implementations

We don鈥檛 plan to allow custom intrinsic implementations.

@andrewbranch why not? Wouldn't e.g. a graphql schema parser be unoptimized written as a type and have concerns about recursion depth? Right now graphql-codegen works but at the very least a non-async pure function seems stable enough.

function mockingly<S extends string>(s: S): intrinsic {
  return s.split('').map((c, i) => c[i % 2 ? 'toUpperCase' : 'toLowerCase']().join(' ')
}

Expect<typeof mockingly("hello world"), "H e L l O   W o R l D">

Wouldn't e.g. a graphql schema parser be unoptimized written as a type and have concerns about recursion depth?

Yes, absolutely. We would not recommend you do that either! 馃槃

Right now graphql-codegen works

Great, problem solved! Preprocessing is our recommendation for this kind of problem in general. We鈥檙e not interested in performing arbitrary code execution during compile time. (At any rate, this discussion is off-topic for this issue, which is asking us to add new instrinsics ourselves.)

Personally standard lib or intrinsic is fine for me as someone who will simply use it. I'll leave it to the TS experts what's better. From my point of view as an end user, lib.d.ts and actual intrinsics are all "intrinsic" in the end-user sense: built-in types that I get to use.

I instead toyed with the idea of a general Split<K, "-">with which one could implement a CamelCase or DashCase oneself: https://github.com/sindresorhus/meow/issues/155#issuecomment-718639981

Enabling something like:

type CamelCase<K extends string> = `${Split<K, "-">[0]}${Capitalize<Split<K, "-"|"_"|" ">[1] | "">}

type CamelCasedProps<T> = {
    [K in keyof T as CamelCase<K>]: () => T[K]
};

interface KebabCased {
    "foo-bar": string;
    foo: number;
}

type CamelCased = CamelCasedProps<KebabCased>;

I think Split<> has to be intrinsic, but I don't think either of CamelCase or KebabCase (isn't that the more common name for DashCase?) has to be intrinsic.

Actually, scratch that, as shown in the top description of https://github.com/microsoft/TypeScript/pull/40336, a Split<> doesn't have to be intrinsic at all but is actually possible right now in the current nightly and with that comes the creation of eg. CamelCase<>.

Trick is to use infer within the template string literal, like:

S extends `${infer T}-${infer U}` ? [T, D] : S

I made a test on the playground which I'm pasting here:

type Split<S extends string, D extends string> =
    string extends S ? string[] :
    S extends '' ? [] :
    S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
    [S];

type SplitOnWordSeparator<T extends string> = Split<T, "-"|"_"|" ">;
type UndefinedToEmptyString<T extends string> = T extends undefined ? "" : T;
type CamelCaseStringArray<K extends string[]> = `${K[0]}${Capitalize<UndefinedToEmptyString<K[1]>>}`;
type CamelCase<K> = K extends string ? CamelCaseStringArray<SplitOnWordSeparator<K>> : K;
type foo3 = CamelCase<"foo-bar">; // Becomes "fooBar"
type foo5 = CamelCase<"foo bar">; // Becomes "fooBar"
type foo6 = CamelCase<"foo_bar">; // Becomes "fooBar"
type foo4 = CamelCase<"foobar">; // Becomes "foobar"

type CamelCasedProps<T> = {
    [K in keyof T as CamelCase<K>]: T[K]
};

interface KebabCased {
    "foo-bar": string;
    foo: number;
}

// Becomes
// {
//    fooBar: string;
//    foo: number;
// }
type CamelCased = CamelCasedProps<KebabCased>;

Sorry for spamming, but just wanted to add an implementation of DashCase/ KebabCase as well. See this updated playground.

It's a bit more awkward as it eg. as to split on all A-Z uppercase chars as well:

type UpperCaseChars = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'X' | 'Y' | 'Z';

@voxpelli great work! The only thing I would point out is that the implementation in your latest linked playground cannot properly handle strings which start with a separator or have multiple separators side by side. Example:

interface KebabCased {
    "-webkit-animation": string;
    "--main-bg-color": string;
    "something--else": string;
}

// Becomes
// {
//    Webkit: string;
//    '': number;
//    something: string;
// }
type CamelCased = CamelCasedProps<KebabCased>;

See this updated playground.

I was working on my own CamelCase utility type as well. It handles those cases, but it's far less generic than what you've come up with.

type Separator = ' ' | '-' | '_';

type CamelCase<T extends string> =
  T extends `${Separator}${infer Suffix}`
  ? CamelCase<Suffix>
  : T extends `${infer Prefix}${Separator}`
  ? CamelCase<Prefix>
  : T extends `${infer Prefix}${Separator}${infer Suffix}`
  ? CamelCase<`${Prefix}${Capitalize<Suffix>}`>
  : T;

type CamelCasedProps<T> = { [K in keyof T as `${CamelCase<string & K>}`]: T[K] }

type SnakeObject = {
  '-webkit-animation': string;
  '--main-bg-color': string;
  'something--else': string;
}

// Becomes
// {
//    webkitAnimation: string;
//    mainBgColor: number;
//    somethingElse: string;
// }
type CamelObject = CamelCasedProps<SnakeObject>;

See this playground.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

blendsdk picture blendsdk  路  3Comments

remojansen picture remojansen  路  3Comments

weswigham picture weswigham  路  3Comments

manekinekko picture manekinekko  路  3Comments

bgrieder picture bgrieder  路  3Comments