Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DashCase and CamelCase intrinsic string types, or similar #40710

Open
5 tasks done
trusktr opened this issue Sep 22, 2020 · 18 comments
Open
5 tasks done

DashCase and CamelCase intrinsic string types, or similar #40710

trusktr opened this issue Sep 22, 2020 · 18 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Milestone

Comments

@trusktr
Copy link
Contributor

trusktr commented Sep 22, 2020

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.
@andrewbranch
Copy link
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?

@andrewbranch andrewbranch added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Sep 23, 2020
@andrewbranch andrewbranch added this to the Backlog milestone Sep 23, 2020
@mmmveggies
Copy link

mmmveggies commented Oct 2, 2020

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

@andrewbranch
Copy link
Member

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

We don’t plan to allow custom intrinsic implementations.

@mmmveggies
Copy link

mmmveggies commented Oct 2, 2020

@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">

@andrewbranch
Copy link
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.)

@trusktr
Copy link
Contributor Author

trusktr commented Oct 14, 2020

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.

@voxpelli
Copy link

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.

@voxpelli
Copy link

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

@voxpelli
Copy link

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

@knpwrs
Copy link

knpwrs commented Nov 23, 2020

@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.

@kdmadej
Copy link

kdmadej commented Feb 12, 2021

@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

@trusktr
Copy link
Contributor Author

trusktr commented Feb 16, 2021

@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?

@trusktr
Copy link
Contributor Author

trusktr commented Feb 16, 2021

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

@voxpelli
Copy link

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

@grumpyTofu
Copy link

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?

@voxpelli
Copy link

@grumpyTofu See these types: https://github.com/sindresorhus/type-fest#template-literal-types

@michalica
Copy link

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

@voxpelli
Copy link

voxpelli commented Apr 8, 2024

@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
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants