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

Template literal types and mapped type 'as' clauses #40336

Merged
merged 35 commits into from Sep 10, 2020
Merged

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Aug 31, 2020

This PR implements two new features:

  • Template literal types, which are a form of string literals with embedded generic placeholders that can be substituted with actual string literals through type instantiation, and
  • Mapped type as clauses, which provide the ability to transform property names in mapped types.

Template literal types

Template literal types are the type space equivalent of template literal expressions. Similar to template literal expressions, template literal types are enclosed in backtick delimiters and can contain placeholders of the form ${T}, where T is a type that is assignable to string, number, boolean, or bigint. Template literal types provide the ability to concatenate literal strings, convert literals of non-string primitive types to their string representation, and change the capitalization or casing of string literals. Furthermore, through type inference, template literal types provide a simple form of string pattern matching and decomposition.

Template literal types are resolved as follows:

  • Union types in placeholders are distributed over the template literal type. For example `[${A|B|C}]` resolves to `[${A}]` | `[${B}]` | `[${C}]`. Union types in multiple placeholders resolve to the cross product. For example `[${A|B},${C|D}]` resolves to `[${A},${C}]` | `[${A},${D}]` | `[${B},${C}]` | `[${B},${D}]`.
  • String, number, boolean, and bigint literal types in placeholders cause the placeholder to be replaced with the string representation of the literal type. For example `[${'abc'}]` resolves to `[abc]` and `[${42}]` resolves to `[42]`.
  • Any one of the types any, string, number, boolean, or bigint in a placeholder causes the template literal to resolve to type string.
  • The type never type in a placeholder causes the template literal to resolve to never.

Some examples:

type EventName<T extends string> = `${T}Changed`;
type Concat<S1 extends string, S2 extends string> = `${S1}${S2}`;
type ToString<T extends string | number | boolean | bigint> = `${T}`;
type T0 = EventName<'foo'>;  // 'fooChanged'
type T1 = EventName<'foo' | 'bar' | 'baz'>;  // 'fooChanged' | 'barChanged' | 'bazChanged'
type T2 = Concat<'Hello', 'World'>;  // 'HelloWorld'
type T3 = `${'top' | 'bottom'}-${'left' | 'right'}`;  // 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
type T4 = ToString<'abc' | 42 | true | -1234n>;  // 'abc' | '42' | 'true' | '-1234'

Beware that the cross product distribution of union types can quickly escalate into very large and costly types. Also note that union types are limited to less than 100,000 constituents, and the following will cause an error:

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Zip = `${Digit}${Digit}${Digit}${Digit}${Digit}`;  // Error

A template literal placeholder may optionally specify an uppercase, lowercase, capitalize, or uncapitalize modifier before the type. This modifier changes the casing of the entire replacement string or the first character of the replacement string. For example:

EDIT: Based on feedback, the casing modifiers have been replaced by intrinsic string types in #40580.

type GetterName<T extends string> = `get${Capitalize<T>}`;
type Cases<T extends string> = `${Uppercase<T>} ${Lowercase<T>} ${Capitalize<T>} ${Uncapitalize<T>}`;
type T10 = GetterName<'foo'>;  // 'getFoo'
type T11 = Cases<'bar'>;  // 'BAR bar Bar bar'
type T12 = Cases<'BAR'>;  // 'BAR bar BAR bAR'

Template literal types are all assignable to and subtypes of string. Furthermore, a template literal type `${T}` is assignable to and a subtype of a template literal type `${C}`, where C is a string literal type constraint of T. For example:

function test<T extends 'foo' | 'bar'>(name: `get${Capitalize<T>}`) {
    let s1: string = name;
    let s2: 'getFoo' | 'getBar' = name;
}

Type inference supports inferring from a string literal type to a template literal type. For inference to succeed the starting and ending literal character spans (if any) of the target must exactly match the starting and ending spans of the source. Inference proceeds by matching each placeholder to a substring in the source from left to right: A placeholder followed by a literal character span is matched by inferring zero or more characters from the source until the first occurrence of that literal character span in the source. A placeholder immediately followed by another placeholder is matched by inferring a single character from the source.

Some examples:

type MatchPair<S extends string> = S extends `[${infer A},${infer B}]` ? [A, B] : unknown;

type T20 = MatchPair<'[1,2]'>;  // ['1', '2']
type T21 = MatchPair<'[foo,bar]'>;  // ['foo', 'bar']
type T22 = MatchPair<' [1,2]'>;  // unknown
type T23 = MatchPair<'[123]'>;  // unknown
type T24 = MatchPair<'[1,2,3,4]'>;  // ['1', '2,3,4']

type FirstTwoAndRest<S extends string> = S extends `${infer A}${infer B}${infer R}` ? [`${A}${B}`, R] : unknown;

type T25 = FirstTwoAndRest<'abcde'>;  // ['ab', 'cde']
type T26 = FirstTwoAndRest<'ab'>;  // ['ab', '']
type T27 = FirstTwoAndRest<'a'>;  // unknown

Template literal types can be combined with recursive conditional types to write Join and Split types that iterate over repeated patterns.

type Join<T extends unknown[], D extends string> =
    T extends [] ? '' :
    T extends [string | number | boolean | bigint] ? `${T[0]}` :
    T extends [string | number | boolean | bigint, ...infer U] ? `${T[0]}${D}${Join<U, D>}` :
    string;
type T30 = Join<[1, 2, 3, 4], '.'>;  // '1.2.3.4'
type T31 = Join<['foo', 'bar', 'baz'], '-'>;  // 'foo-bar-baz'
type T32 = Join<[], '.'>;  // ''
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 T40 = Split<'foo', '.'>;  // ['foo']
type T41 = Split<'foo.bar.baz', '.'>;  // ['foo', 'bar', 'baz']
type T42 = Split<'foo.bar', ''>;  // ['f', 'o', 'o', '.', 'b', 'a', 'r']
type T43 = Split<any, '.'>;  // string[]

The recursive inference capabilities can for example be used to strongly type functions that access properties using "dotted paths", and pattern that is sometimes used in JavaScript frameworks.

type PropType<T, Path extends string> =
    string extends Path ? unknown :
    Path extends keyof T ? T[Path] :
    Path extends `${infer K}.${infer R}` ? K extends keyof T ? PropType<T[K], R> : unknown :
    unknown;

declare function getPropValue<T, P extends string>(obj: T, path: P): PropType<T, P>;
declare const s: string;

const obj = { a: { b: {c: 42, d: 'hello' }}};
getPropValue(obj, 'a');  // { b: {c: number, d: string } }
getPropValue(obj, 'a.b');  // {c: number, d: string }
getPropValue(obj, 'a.b.d');  // string
getPropValue(obj, 'a.b.x');  // unknown
getPropValue(obj, s);  // unknown

Mapped type as clauses

With this PR, mapped types support an optional as clause through which a transformation of the generated property names can be specified:

{ [P in K as N]: X }

where N must be a type that is assignable to string | number | symbol. Typically, N is a type that transforms P, such as a template literal type that uses P in a placeholder. For example:

type Getters<T> = { [P in keyof T & string as `get${Capitalize<P>}`]: () => T[P] };
type T50 = Getters<{ foo: string, bar: number }>;  // { getFoo: () => string, getBar: () => number }

Above, a keyof T & string intersection is required because keyof T could contain symbol types that cannot be transformed using template literal types.

When the type specified in an as clause resolves to never, no property is generated for that key. Thus, an as clause can be used as a filter:

type Methods<T> = { [P in keyof T as T[P] extends Function ? P : never]: T[P] };
type T60 = Methods<{ foo(): number, bar: boolean }>;  // { foo(): number }

When the type specified in an as clause resolves to a union of literal types, multiple properties with the same type are generated:

type DoubleProp<T> = { [P in keyof T & string as `${P}1` | `${P}2`]: T[P] }
type T70 = DoubleProp<{ a: string, b: number }>;  // { a1: string, a2: string, b1: number, b2: number }

Fixes #12754.


Playground: https://www.typescriptlang.org/play?ts=4.1.0-pr-40336-88

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Aug 31, 2020
@ahejlsberg ahejlsberg added this to the TypeScript 4.1.0 milestone Aug 31, 2020
@alii
Copy link

alii commented Aug 31, 2020

Looks incredible! Nice work.

@taxilian
Copy link

taxilian commented Aug 31, 2020

very cool, thanks for doing this! It will be tricky to use this well without exceeding the "50 steps" limit with libraries like mongoose, but still will enable a lot of great things

@bschlenk
Copy link

bschlenk commented Aug 31, 2020

This looks really good! Will there be a way to add more modifiers? The times I’ve wanted this feature it’s been to convert from ALL_CAPS to camelCase.

@calebeby
Copy link

calebeby commented Aug 31, 2020

@bschlenk It seems like that should be implementable by users since split/join can be implemented as shown in the PR description

@calebeby
Copy link

calebeby commented Aug 31, 2020

Can we get a playground for this PR?

Not sure if this will work since I'm not a part of the TS team:

@typescript-bot pack this

@orta
Copy link
Contributor

orta commented Aug 31, 2020

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Aug 31, 2020

Heya @orta, I've started to run the tarball bundle task on this PR at c95c000. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Aug 31, 2020

Hey @orta, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/83806/artifacts?artifactName=tgz&fileId=AFC60CCD5AEE4CC19D6C15D9044564BC4E2AEA5FE84FC89B8DB3C2386C10DB0202&fileName=/typescript-4.1.0-insiders.20200831.tgz"
    }
}

and then running npm install.


There is also a playground for this build.

@danvk
Copy link

danvk commented Sep 1, 2020

Is it possible to split a string literal type into a tuple of its characters? If you could split "foo" into ["f", "oo"] then you wouldn't need to special case capitalize and uncapitalize. (A typed camelCase function has always been my go-to example of something that's too complex for TS to type, but it seems very, very close!)

@rickbutton
Copy link

rickbutton commented Sep 1, 2020

@danvk converting between camelCase, snake-case, PascalCase etc is already possible with this PR: here is a playground link

@danvk
Copy link

danvk commented Sep 1, 2020

Thanks @rickbutton! I'm wondering if it's possible to go the other way, though: FooBarfoo-bar or foo_bar.

@g-plane
Copy link
Contributor

g-plane commented Sep 1, 2020

For the mapped type as clauses, the current behavior of compiler is:

type DoubleProp<T> = { [P in keyof T & string as `${P}1` | `${P}2`]: T[P] }
type T70 = DoubleProp<{ a: string, b: number }>

type Keys = keyof T70  // ==> 'a' | 'b'

Is this intended? Why isn't 'a1' | 'a2' | 'b1' | 'b2'?

@mh-alahdadian
Copy link

mh-alahdadian commented Nov 27, 2020

does anyone know how to create a type to show any subset of a union?
example

type Union = "A" | "B" | "C";
type X<U> = ?;
type TEST = X<Union> // "A" | "A,B" | "A,C" | "B" | "B,C" | "C" ...

we were waiting for it very long time and now we have template types

maybe it is a good idea to have a builtin type like Capitalize and ... to convert tuple or unions to string;

@Yurickh
Copy link

Yurickh commented Nov 27, 2020

type Union = "A" | "B" | "C";
type X<U extends string> = `${U},${U}`
type TEST = X<Union>

Will give you all combinations of Union over itself, which means you'll get A,A instead of A.

@Yurickh
Copy link

Yurickh commented Nov 27, 2020

type Union = "A" | "B" | "C";
type Y<U extends string, T extends string = U> = U extends string ? T extends U ? T : `${U},${T}` : never
type TEST2 = Y<Union>

Does exactly what you want though :)~

@mh-alahdadian
Copy link

mh-alahdadian commented Nov 28, 2020

type Union = "A" | "B" | "C";
type Y<U extends string, T extends string = U> = U extends string ? T extends U ? T : `${U},${T}` : never
type TEST2 = Y<Union>

Does exactly what you want though :)~

thanks for your response
this type will only give me subset with 1 or 2 child; but I want any subset of union including 3 member and more for bigger unions

@gausie
Copy link

gausie commented Dec 1, 2020

Is it possible to use this new functionality the other way round and type something against a union of string literals case-insensitively?

type CaseInsensitive<T extends string> = ???;

type ExampleUnion = "alpha" | "beta" | "gamma";

const example: CaseInsensitive<ExampleUnion> = "aLPhA";

@jcalz
Copy link
Contributor

jcalz commented Dec 1, 2020

@gausie you could do what I did in this StackOverflow answer:

// union of all possible strings that match T in a case-insensitive way
type CaseInsensitive<T extends string> =
    string extends T ? string :
    T extends `${infer F1}${infer F2}${infer R}` ? (
        `${Uppercase<F1> | Lowercase<F1>}${Uppercase<F2> | Lowercase<F2>}${CaseInsensitive<R>}`
    ) :
    T extends `${infer F}${infer R}` ? `${Uppercase<F> | Lowercase<F>}${CaseInsensitive<R>}` :
    ""

type ExampleUnion = "alpha" | "beta" | "gamma";

const example: CaseInsensitive<ExampleUnion> = "aLPhA";
const err: CaseInsensitive<ExampleUnion> = "alfha"; // error!

// CaseInsensitive<ExampleUnion> has 2⁵ + 2⁴ + 2⁵ = 80 members 
// that's fine, but it doesn't scale well
// type Oops = CaseInsensitive<"eighteenlettersaaa"> // ⏳ compiler bogged down, 2​¹⁸ = 262,144 members
// type Oops = CaseInsensitive<"nineteenlettersaaaa"> // error! union too complex to represent​​



// The following generic approach scales better because it just checks one value 
// instead of generating all possible values

const asCaseInsensitive = <U extends string>() => <T extends string>(
    val: Lowercase<T> extends Lowercase<U> ? T : U
) => val;
 
const asCaseInsensitiveExample = asCaseInsensitive<ExampleUnion>();

const example2 = asCaseInsensitiveExample("aLPhA"); // okay

// scales fine
asCaseInsensitive<"nineteenlettersaaaa">()("NineteenLettersAAAA"); // okay
asCaseInsensitive<"nineteenlettersaaaa">()("NineteenLetterzAAAA"); // error

Playground

@gausie
Copy link

gausie commented Dec 2, 2020

@gausie you could do what I did in this StackOverflow answer:

[Playground](https://www.typescriptlang.org/play?#code/PTAEFcDsEsHtNLAZqAhgG3aADrAzntAEboCmoeALgE7SQDmeolAFqpaALbsDGLoAFVB00oHqjykAtHUmRClaADdyAd1QBPAFCUN2cgGEJpAJLzS86IpUAeIaQAelCwBMmVWgwB8oALxbQQIoaOnpQR2dIN0FQAH5gzzCALgCg+ydXJgADABIAbzokUmpQADEARgBffMLisoAmaoLIIpKAJUqsuNAAClSgoNy8gFVsfWpxSRsKnwAfUAAZWFViydJp8q8m0fG16fq5xeXV432t-KNJMzkFZXW2raz+0ABKUBSB9MjoodqS0qaf1AHS68SGOxOU1KhyWKwmp2hTUupnMlms90e72eACJsVodHpyABRByoTjYMjDGDwPygbEYbBsbGgebYoikSioZms+hk7jYgDc+J48Co4VJ5LISVAyOuFluthJZIppCpcEgPl8dNQCwACiwAIKCrQi+QcYrUaWy1EK9ZKyWq6ka2n09BIJkC0AgcLUaiwagAQnx3utNysdxs9pVavgPjYTHqgFcCUAAalA9UALgSp9PJrUADgADFxSJx2dQmFpvax2AByJhIOikAA0oCI4A4VlALlgpDwkBrHDw4jIoBWmErYF0+lAAHlYNgmFrQ-Lw7ZsaRoPQWM4LGRKM5y6gj9ifN7AM-EYlg5OgZBKRFg9HopBcXeWkBb9UA0AQATsAHgS0+oADZ6ibcoABYwOLUtijwCdmEJWd50XGVjDlNEI2xGBIA5Uhdw5A88CPY9TzAC1-QDCAnWYWBYEvB0HGo0BqFIbBmLkShP0-fFgzAAQWHIJBYEwZZQlAJ9sNoHg0DGP1UD4Chh17Vt8LqdlxHASRhA4AArDSOD4UgeAAayYeByCUDBwHIODZGcVAX2QMSLGKdhRIwLBcAIYgRws9ArNgk1RQ4CRl3QlRaRsYZxW+dwQm8Ho3l8Hw7GizIElCLw+gGXzpVhSF1gEHwIjSvL4SmYYfHiIRpWGLREp8XyhVAQKzTQPBQttKMRy1ELUJtVc7QlaMnUyl4hRasVHGVMh6lpXqrn69EutIHp6T1Q1sTGr0wFgIzNB4hSMCUhtsK0eaUTDdEbEwxsd0gPcCKIrlRtWgA5W7cMgBYVPLA0-qNLbvV2-bzrQ21rqwnC8P3GCnpPBK3o+ixvph6gAC9-oBz1vTI6gtCAA)

This is awesome! But probably not great for my use case trying to type check the input to a library function

@fc01
Copy link

fc01 commented Sep 17, 2021

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