-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Intrinsic string types #40580
Conversation
# Conflicts: # src/compiler/diagnosticMessages.json
Total bikeshedding and possibly a bad idea, but... would it make any sense to re-use
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. |
That's certainly looks reasonable, but unlike the current |
There was a problem hiding this 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);
type intrinsic = string;
let a1: intrinsic = "ok"; should probably also be checked to still work, right? |
As with the |
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. |
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 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 |
The |
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({ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 👀
There was a problem hiding this comment.
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.
Yeah, it would be great to also have In the DOM libs it is common to map from JS camelCase properties to DOM dash-case attributes (f.e. |
FWIW, if you want to play around with these types in https://www.typescriptlang.org/play @ 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}`; |
Think we can add something like 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 using 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]>;
}; |
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. |
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; |
This PR introduces four new intrinsic string mapping types in
lib.es5.d.ts
:The new
intrinsic
keyword is used to indicate that the type alias references a compiler provided implementation. It is an error to specifyintrinsic
anywhere but immediately following the=
separator in a type alias declaration for a type namedUppercase
,Lowercase
,Capitalize
orUncapitalize
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 useintrinsic
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:
Note that the
Capitalize<S>
andUncapitalize<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
, anduncapitalize
modifiers in template literal types (based on feedback in #40336). This PR removes those modifiers.