Skip to content

DashCase and CamelCase intrinsic string types, or similar #40710

Open

Description

@trusktr

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:

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

Activity

  1. andrewbranch commented on Sep 23, 2020

    @andrewbranch
    Member

    Something that’s important to clarify in this suggestion is whether it’s 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?

  2. mmmveggies commented on Oct 2, 2020

    @mmmveggies

    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

  3. andrewbranch commented on Oct 2, 2020

    @andrewbranch
    Member

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

    We don’t plan to allow custom intrinsic implementations.

  4. mmmveggies commented on Oct 2, 2020

    @mmmveggies

    Andrew Branch (@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">
  5. andrewbranch commented on Oct 2, 2020

    @andrewbranch
    Member

    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’re 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.)

  6. trusktr commented on Oct 14, 2020

    @trusktr
    ContributorAuthor

    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.

  7. voxpelli commented on Oct 29, 2020

    @voxpelli

    I instead toyed with the idea of a general Split<K, "-">with which one could implement a CamelCase or DashCase oneself: sindresorhus/meow#155 (comment)

    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.

  8. voxpelli commented on Oct 29, 2020

    @voxpelli

    Actually, scratch that, as shown in the top description of #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>;
  9. voxpelli commented on Oct 29, 2020

    @voxpelli

    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';
  10. knpwrs commented on Nov 23, 2020

    @knpwrs

    Pelle Wessman (@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.

  11. kdmadej commented on Feb 12, 2021

    @kdmadej

    Ken Powers (@knpwrs) your solution kills the playground tab in my browser (and the TS language service in my VSC 😅 ) when I use the following keys:

    'Awa_bbb-ccc' |  '-Awa_bbb-ccc' | '_Awa_bbb-ccc'
    

    I came up with my own implementation and it seems to be working better for strings up to 15 characters long. Unfortunately anything longer than that triggers the

    Type instantiation is excessively deep and possibly infinite.ts(2589)
    

    error 😞 (but doesn't kill the IDE 😅 )

    playground link

  12. trusktr commented on Feb 16, 2021

    @trusktr
    ContributorAuthor

    Pelle Wessman (@voxpelli) Your solution has been working super great, but today I ran into a weird one. Based on your code, I tried this:

    type DashCasedProps<T> = {
        [K in keyof T as KebabCase<K>]: T[K]
    };
    
    type test = DashCasedProps<{dracoDecorderPath: number}>

    And the resulting test type is

    type test = {
        dracoDecorderPath: number;
        dracoDecorderDecorderPath: number;
        "draco-dath": number;
        "draco-path": number;
        "dracoDecorder-dath": number;
        "dracoDecorder-path": number;
        dracoDecorderPecorderath: number;
        "draco-decorderath": number;
        "draco-pecorderath": number;
        dracoDecorderDecorderPecorderath: number;
        "dracoDecorder-decorderath": number;
        "dracoDecorder-pecorderath": number;
    }

    playground link (see at the bottom)

    Anyone know why that happens with that particular example?

  13. trusktr commented on Feb 16, 2021

    @trusktr
    ContributorAuthor

    Ken Powers (@knpwrs) Expanding on your example, what would the opposite SnakeCasedProps type look like?

  14. voxpelli commented on Feb 16, 2021

    @voxpelli

    Joe Pea (@trusktr) Is the same issue happening with the version of it that we're maintaining in https://github.com/sindresorhus/type-fest ?

  15. grumpyTofu commented on Oct 13, 2021

    @grumpyTofu

    Is there any interest in making it possible to write custom intrinsic string types? For popular libraries like Material UI, it would be really helpful (i.e. below):

    const UnCamelCase = (str: string) => str.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
    
    interface CustomObject {
      id: number;
      name: string;
      someValue: string;
    }
    
    interface CustomGridColDef extends GridColDef {
      field: keyof CustomObject;
      headerName: UnCamelCase<keyof CustomObject>;
    }
    
    const columns: CustomGridColDef[] = [
      { field: "id", headerName: "Id", flex: 1, minWidth: 150, hide: true },
      { field: "name", headerName: "Name", flex: 1, minWidth: 150 },
      { field: "someValue", headerName: "Some Value", flex: 1, minWidth: 150 },
    ];
    
    const SomeComponent = () => <DataGrid rows={data} columns={columns}))} />

    Or perhaps a Typescript guru would be able to come up with a hack for this that is eluding me?

  16. voxpelli commented on Oct 13, 2021

    @voxpelli
  17. michalica commented on Apr 8, 2024

    @michalica

    Pelle Wessman (@voxpelli) thx for the good work. Your implementation works only If word has only two segments. I upgraded it to work with an endless number of segments

    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> = T extends undefined ? "" : T;
    type CapitalizeStringsInTuple<T extends any[]> =
        T extends [infer First, ...infer Rest] ? 
            [First, ...(Rest extends string[] ? CapitalizeStringsInTuple<Rest> : [])] :
            [];
    
    // Converts an array of strings to a camelCase string
    type JoinStringsAsCamelCase<T extends any[], Prev extends string = ''> =
        T extends [infer First, ...infer Rest] ? 
            Rest extends string[] ? 
                JoinStringsAsCamelCase<Rest, `${Prev}${First extends string ? Capitalize<First> : ''}`> :
                `${Prev}${First}` :
            Prev;
    
    type CamelCase<K> = K extends string ? JoinStringsAsCamelCase<SplitOnWordSeparator<K>> : K;
  18. voxpelli commented on Apr 8, 2024

    @voxpelli

    michalica 👍 You can find the latest iteration of mine in the type-fest module, and PR:s are welcome there 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions