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

Intrinsic string types #40580

Merged
merged 22 commits into from Sep 21, 2020
Merged

Intrinsic string types #40580

merged 22 commits into from Sep 21, 2020

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Sep 16, 2020

This PR introduces four new intrinsic string mapping types in lib.es5.d.ts:

type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;

The new intrinsic keyword is used to indicate that the type alias references a compiler provided implementation. It is an error to specify intrinsic anywhere but immediately following the = separator in a type alias declaration for a type named Uppercase, Lowercase, Capitalize or Uncapitalize taking a single type parameter (but, of course, it is possible that additional intrinsic implementations will be provided in the future). There is generally no reason to ever use intrinsic in user code.

The intrinsic string types behave just like ordinary generic types and are similar to distributive conditional types in that they distribute over union types. Some examples:

type T10 = Uppercase<'hello'>;  // "HELLO"
type T11 = Lowercase<'HELLO'>;  // "hello"
type T12 = Capitalize<'hello'>;  // "Hello"
type T13 = Uncapitalize<'Hello'>;  // "hello"

type T20 = Uppercase<'foo' | 'bar'>;  // "FOO" | "BAR"
type T21 = Lowercase<'FOO' | 'BAR'>;  // "foo" | "bar"
type T22 = Capitalize<'foo' | 'bar'>;  // "Foo" | "Bar"
type T23 = Uncapitalize<'Foo' | 'Bar'>;  // "foo" | "bar"

type T30<S extends string> = Uppercase<`aB${S}`>;
type T31 = T30<'xYz'>;  // "ABXYZ"
type T32<S extends string> = Lowercase<`aB${S}`>;
type T33 = T32<'xYz'>;  // "abxyz"
type T34 = `${Uppercase<'abc'>}${Lowercase<'XYZ'>}`;  // "ABCxyz"

type T40 = Uppercase<string>;  // string
type T41 = Uppercase<any>;  // any
type T42 = Uppercase<never>;  // never
type T43 = Uppercase<42>;  // Error, type 'number' does not satisfy the constraint 'string'

Note that the Capitalize<S> and Uncapitalize<S> intrinsic types could fairly easily be implemented in pure TypeScript using conditional types and template literal type inference, but it isn't practical to do so at the moment because we use ESLint which hasn't yet been updated to support template literal types (though we expect that to happen soon).

The intrinsic string types replace the uppercase, lowercase, capitalize, and uncapitalize modifiers in template literal types (based on feedback in #40336). This PR removes those modifiers.

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Sep 16, 2020
# Conflicts:
#	src/compiler/diagnosticMessages.json
@ethanresnick
Copy link
Contributor

Total bikeshedding and possibly a bad idea, but... would it make any sense to re-use declare, as in:

declare type Capitalize<T extends string>;

I don't know declare's current semantics well enough to say whether that's a natural extension, or whether it's a confusing jumble that would overload declare with too many similar, but not quite close enough, meanings. Or maybe this already means something else. But just a thought.

@ahejlsberg
Copy link
Member Author

Would it make any sense to re-use declare...

That's certainly looks reasonable, but unlike the current intrinsic solution, it would require a tool chain update because our grammar currently always requires an = and a type in a type alias declaration. For example, we'd need ESLint to be updated.

Copy link
Member

@DanielRosenwasser DanielRosenwasser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get some tests for intrinsic being used outside of the immediate type alias context?

let a1: intrinsic;
let a2: { intrinsic: intrinsic };
let intrinsic: intrinsic.intrinsic;
type Foo = (intrinsic);
type Foo<intrinsic> = intrinsic;
type Foo<T extends intrinsic> = T;
type Foo<intrinsic extends intrinsic> = intrinsic;
type Bar<intrinsic extends intrinsic> = (intrinsic);

@weswigham
Copy link
Member

type intrinsic = string;
let a1: intrinsic = "ok";

should probably also be checked to still work, right?

@rbuckton
Copy link
Member

As with the declare suggestion, I would still like to express my preference for extern type Uppercase<T extends string>; it still would cause issues with tooling, but we could look into what's necessary to put together and use a "private" build of typescript-eslint to handle that.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Sep 16, 2020

Tooling catchup concerns aren't reasonable blockers in my opinion. We could get the self-hosting blockers out of the way ahead of time if we really needed, and that's the right workflow and right way to work with the community.

@rbuckton
Copy link
Member

The only "tooling catchup concern" I think we're discussing is regarding tooling that out team depends on to build the product. Adding new syntax often requires a full release cycle lag before we can actually use that syntax in our declaration files, given that we use eslint and typescript-eslint to lint those files.

When we added compound assignment operators, we couldn't use them in our own codebase until typescript-eslint/typescript-eslint#2253 was merged, and that required us to use a nightly build until the next official release went out. At the very least, making PRs against typescript-eslint to unblock ourselves still benefits the community as it is one fewer tool in the toolchain that users will need to wait for before they can upgrade to a newer version of TypeScript.

@rbuckton
Copy link
Member

The typescript-eslint concern is also the reason why we are looking for a way to externalize the type for Capitalize, as implementing Capitalize in terms of Uppercase using template literal types also breaks typescript-eslint due to the newer syntax.

@treybrisbane
Copy link

I really like this approach. Much more than the previous "magic modifiers"! 🙂

Having a relatively general solution for special-casing complex typing operations without the need for new syntax, that also gives you a free migration path if/when the language becomes expressive enough to implement them outside the compiler, seems like a win for everybody.

Uncapitalize
}

const intrinsicTypeKinds: ReadonlyESMap<string, IntrinsicTypeKind> = new Map(getEntries({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also change ThisType into an intrinsic?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? ThisType is just a marker type that doesn't have any effect on its type argument. No reason to change it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought "intrinsic" means "hey I'm compiler magic" and ThisType is a compiler magic too 👀

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several "magic" types already that aren't intrinsic. For example Object, Function, Array<T>, Promise<T>, Iterator<T>, and others all receive special treatment in one way or another. The particular role "intrinsic" plays is to indicate that the implementation of the type is provided by the compiler. In the case of ThisType, there really is nothing interesting about the implementation because ThisType<T> is the same as T. All we do with ThisType is to activate certain behaviors when a reference to it occurs in a contextual type, but otherwise there's nothing special about it.

@ahejlsberg ahejlsberg merged commit fbce4f6 into master Sep 21, 2020
@ahejlsberg ahejlsberg deleted the intrinsicStringTypes branch September 21, 2020 17:09
@Jack-Works Jack-Works mentioned this pull request Sep 22, 2020
@trusktr
Copy link

trusktr commented Sep 22, 2020

Yeah, it would be great to also have DashCase and CamelCase and similar!

In the DOM libs it is common 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>).

@joepvl
Copy link

joepvl commented Nov 18, 2020

FWIW, if you want to play around with these types in https://www.typescriptlang.org/play @ v4.1.0-beta, you can emulate them using the following:

type Uncapitalize<S extends string> = `${uncapitalize S}`;
type Capitalize<S extends string> = `${capitalize S}`;
type Lowercase<S extends string> = `${lowercase S}`;
type Uppercase<S extends string> = `${uppercase S}`;

@radiosilence
Copy link

radiosilence commented Nov 20, 2020

Think we can add something like Camelize or SnakeCase to convert from snake_case to camelCase etc? It would be cool if we could define our own somehow.

Would be really sweet if we could finally type a recursive camelize function.

This does it for a fixed word length:

type B = 'hi_mate';
type Camelize<S extends string> = S extends `${infer A}_${infer B}`
  ? `${A}${Capitalize<B>}`
  : unknown;

type C = Camelize<B>;

Is there a way to use spread to do it for many?

Have managed to do it like this but it's hideous:

type Z = 'hi_mate_what_up_good_morning_today';

type Camelize<
  S
> = S extends `${infer A}_${infer B}_${infer C}_${infer D}_${infer E}_${infer F}_${infer G}_${infer H}`
  ? `${A}${Capitalize<B>}${Capitalize<C>}${Capitalize<D>}${Capitalize<E>}${Capitalize<F>}${Capitalize<G>}${Capitalize<H>}`
  : S extends `${infer A}_${infer B}_${infer C}_${infer D}_${infer E}_${infer F}_${infer G}`
  ? `${A}${Capitalize<B>}${Capitalize<C>}${Capitalize<D>}${Capitalize<E>}${Capitalize<F>}${Capitalize<G>}`
  : S extends `${infer A}_${infer B}_${infer C}_${infer D}_${infer E}_${infer F}`
  ? `${A}${Capitalize<B>}${Capitalize<C>}${Capitalize<D>}${Capitalize<E>}${Capitalize<F>}`
  : S extends `${infer A}_${infer B}_${infer C}_${infer D}_${infer E}`
  ? `${A}${Capitalize<B>}${Capitalize<C>}${Capitalize<D>}${Capitalize<E>}`
  : S extends `${infer A}_${infer B}_${infer C}_${infer D}`
  ? `${A}${Capitalize<B>}${Capitalize<C>}${Capitalize<D>}`
  : S extends `${infer A}_${infer B}_${infer C}`
  ? `${A}${Capitalize<B>}${Capitalize<C>}`
  : S extends `${infer A}_${infer B}`
  ? `${A}${Capitalize<B>}`
  : S;

type C = Camelize<Z>; // hiMateWhatUpGoodMorningToday

function upperFirst<S extends string>(s: S) {
  const [head, ...tail] = s;
  return [head.toUpperCase(), ...tail].join('') as Capitalize<S>;
}

const z = upperFirst('hello');

function camelCase<S extends string>(s: S) {
  const [head, ...tail] = s.split('_');
  return `${head}${tail.map(upperFirst).join('')}` as Camelize<S>;
}

const y = camelCase('hello_mate');

So far so good, now I just have to figure out a way of doing the typing recursively on an object.

Ok, I think I nailed it:

type DeepCamelize<T> = {
  [K in keyof T as Camelize<K>]: DeepCamelize<T[K]>;
};

Would still be nice to get: Same in reverse, and maybe an implementation that uses spread.

Ok, improved version usingJoin and a modified splitter:

type CapitalizeIf<
  Condition extends boolean,
  T extends string
> = Condition extends true ? Capitalize<T> : T;

type SplitCamel<
  S extends string,
  D extends string,
  IsTail extends boolean = false
> = string extends S
  ? string[]
  : S extends ''
  ? []
  : S extends `${infer T}${D}${infer U}`
  ? [CapitalizeIf<IsTail, T>, ...SplitCamel<U, D, true>]
  : [CapitalizeIf<IsTail, S>];

type Camelize<S> = S extends string ? Join<SplitCamel<S, '_'>, ''> : S;


type DeepCamelize<T> = {
  [K in keyof T as Camelize<K>]: DeepCamelize<T[K]>;
};

sheetalkamat added a commit to microsoft/TypeScript-TmLanguage that referenced this pull request Nov 20, 2020
@aboyton
Copy link

aboyton commented Nov 24, 2020

For camelCase, this seems to be an example in the PR that added this feature. https://github.com/microsoft/TypeScript/pull/40336/files#diff-4c1d1a787d1d286623e4419c6b614fc45fc6f65d7ff4efde25e5d145f9e0c654R84

type SnakeToCamelCase<S extends string> =
    S extends `${infer T}_${infer U}` ? `${lowercase T}${SnakeToPascalCase<U>}` :
    S extends `${infer T}` ? `${lowercase T}` :
    SnakeToPascalCase<S>;

type SnakeToPascalCase<S extends string> =
    string extends S ? string :
    S extends `${infer T}_${infer U}` ? `${capitalize `${lowercase T}`}${SnakeToPascalCase<U>}` :
    S extends `${infer T}` ? `${capitalize `${lowercase T}`}` :
    never;

type RR0 = SnakeToPascalCase<'hello_world_foo'>;  // 'HelloWorldFoo'
type RR1 = SnakeToPascalCase<'FOO_BAR_BAZ'>;  // 'FooBarBaz'
type RR2 = SnakeToCamelCase<'hello_world_foo'>;  // 'helloWorldFoo'
type RR3 = SnakeToCamelCase<'FOO_BAR_BAZ'>;  // 'fooBarBaz'

This could be useful to be shared inside TypeScript itself.

@mikaelwaltersson
Copy link

mikaelwaltersson commented Feb 16, 2021

This should work for strings, objects and arrays:

type ToCamelCase<T> =
    T extends `${infer A}_${infer B}`
        ? `${Uncapitalize<A>}${Capitalize<ToCamelCase<B>>}` :
    T extends string
        ? Uncapitalize<T> :
    T extends (infer A)[]
        ? ToCamelCase<A>[] :
    T extends {}
        ?  { [K in keyof T as ToCamelCase<K>]: ToCamelCase<T[K]>; } :
    T;
type ToSnakeCase<T> =
    T extends `${infer A}${infer B}${infer C}`
        ? (
           [A, B, C] extends [Lowercase<A>, Exclude<Uppercase<B>, '_'>, C]
                ? `${A}_${Lowercase<B>}${ToSnakeCase<C>}`
                : `${Lowercase<A>}${ToSnakeCase<`${B}${C}`>}`
        ) :
    T extends string
        ? Lowercase<T> :
    T extends (infer A)[]
        ? ToSnakeCase<A>[] :
    T extends {}
        ?  { [K in keyof T as ToSnakeCase<K>]: ToSnakeCase<T[K]>; } :
    T;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet