Skip to content

Commit

Permalink
Template literal types for template literal expressions (#41891)
Browse files Browse the repository at this point in the history
* Infer template literal types for template literal expressions

* Update test

* Update another test

* Accept new baselines

* Minor fixes

* Add tests

* Accept new baselines

* Make new TypeFlags internal

* Accept new API baselines

* Ensure template literals assignable to String, Object, {}, etc.

* Add tests
  • Loading branch information
ahejlsberg authored Dec 12, 2020
1 parent 78ded65 commit ee1f262
Show file tree
Hide file tree
Showing 212 changed files with 2,088 additions and 1,016 deletions.
63 changes: 35 additions & 28 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13187,7 +13187,7 @@ namespace ts {
i--;
const t = types[i];
const remove =
t.flags & TypeFlags.StringLiteral && includes & TypeFlags.String ||
t.flags & TypeFlags.StringLikeLiteral && includes & TypeFlags.String ||
t.flags & TypeFlags.NumberLiteral && includes & TypeFlags.Number ||
t.flags & TypeFlags.BigIntLiteral && includes & TypeFlags.BigInt ||
t.flags & TypeFlags.UniqueESSymbol && includes & TypeFlags.ESSymbol ||
Expand Down Expand Up @@ -13234,7 +13234,7 @@ namespace ts {
}
switch (unionReduction) {
case UnionReduction.Literal:
if (includes & (TypeFlags.Literal | TypeFlags.UniqueESSymbol)) {
if (includes & (TypeFlags.FreshableLiteral | TypeFlags.UniqueESSymbol)) {
removeRedundantLiteralTypes(typeSet, includes);
}
if (includes & TypeFlags.StringLiteral && includes & TypeFlags.TemplateLiteral) {
Expand Down Expand Up @@ -13765,6 +13765,7 @@ namespace ts {
let type = templateLiteralTypes.get(id);
if (!type) {
templateLiteralTypes.set(id, type = createTemplateLiteralType(newTexts, newTypes));
type.regularType = type;
}
return type;

Expand Down Expand Up @@ -14803,26 +14804,28 @@ namespace ts {
}

function getFreshTypeOfLiteralType(type: Type): Type {
if (type.flags & TypeFlags.Literal) {
if (!(<LiteralType>type).freshType) {
const freshType = createLiteralType(type.flags, (<LiteralType>type).value, (<LiteralType>type).symbol);
freshType.regularType = <LiteralType>type;
if (type.flags & TypeFlags.FreshableLiteral) {
if (!(<FreshableLiteralType>type).freshType) {
const freshType = type.flags & TypeFlags.TemplateLiteral ?
createTemplateLiteralType((<TemplateLiteralType>type).texts, (<TemplateLiteralType>type).types) :
createLiteralType(type.flags, (<LiteralType>type).value, (<LiteralType>type).symbol);
freshType.regularType = <FreshableLiteralType>type;
freshType.freshType = freshType;
(<LiteralType>type).freshType = freshType;
(<FreshableLiteralType>type).freshType = freshType;
}
return (<LiteralType>type).freshType;
return (<FreshableLiteralType>type).freshType;
}
return type;
}

function getRegularTypeOfLiteralType(type: Type): Type {
return type.flags & TypeFlags.Literal ? (<LiteralType>type).regularType :
return type.flags & TypeFlags.FreshableLiteral ? (<FreshableLiteralType>type).regularType :
type.flags & TypeFlags.Union ? ((<UnionType>type).regularType || ((<UnionType>type).regularType = getUnionType(sameMap((<UnionType>type).types, getRegularTypeOfLiteralType)) as UnionType)) :
type;
}

function isFreshLiteralType(type: Type) {
return !!(type.flags & TypeFlags.Literal) && (<LiteralType>type).freshType === type;
return !!(type.flags & TypeFlags.FreshableLiteral) && (<FreshableLiteralType>type).freshType === type;
}

function getLiteralType(value: string): StringLiteralType;
Expand Down Expand Up @@ -17694,17 +17697,20 @@ namespace ts {
}
}
else if (source.flags & TypeFlags.TemplateLiteral) {
if (target.flags & TypeFlags.TemplateLiteral &&
(source as TemplateLiteralType).texts.length === (target as TemplateLiteralType).texts.length &&
(source as TemplateLiteralType).types.length === (target as TemplateLiteralType).types.length &&
every((source as TemplateLiteralType).texts, (t, i) => t === (target as TemplateLiteralType).texts[i]) &&
every((instantiateType(source, makeFunctionTypeMapper(reportUnreliableMarkers)) as TemplateLiteralType).types, (t, i) => !!((target as TemplateLiteralType).types[i].flags & (TypeFlags.Any | TypeFlags.String)) || !!isRelatedTo(t, (target as TemplateLiteralType).types[i], /*reportErrors*/ false))) {
return Ternary.True;
if (target.flags & TypeFlags.TemplateLiteral) {
if ((source as TemplateLiteralType).texts.length === (target as TemplateLiteralType).texts.length &&
(source as TemplateLiteralType).types.length === (target as TemplateLiteralType).types.length &&
every((source as TemplateLiteralType).texts, (t, i) => t === (target as TemplateLiteralType).texts[i]) &&
every((instantiateType(source, makeFunctionTypeMapper(reportUnreliableMarkers)) as TemplateLiteralType).types, (t, i) => !!((target as TemplateLiteralType).types[i].flags & (TypeFlags.Any | TypeFlags.String)) || !!isRelatedTo(t, (target as TemplateLiteralType).types[i], /*reportErrors*/ false))) {
return Ternary.True;
}
}
const constraint = getBaseConstraintOfType(source);
if (constraint && constraint !== source && (result = isRelatedTo(constraint, target, reportErrors))) {
resetErrorInfo(saveErrorInfo);
return result;
else {
const constraint = getBaseConstraintOfType(source);
if (result = isRelatedTo(constraint && constraint !== source ? constraint : stringType, target, reportErrors)) {
resetErrorInfo(saveErrorInfo);
return result;
}
}
}
else if (source.flags & TypeFlags.StringMapping) {
Expand Down Expand Up @@ -19181,7 +19187,7 @@ namespace ts {

function getBaseTypeOfLiteralType(type: Type): Type {
return type.flags & TypeFlags.EnumLiteral ? getBaseTypeOfEnumLiteralType(<LiteralType>type) :
type.flags & TypeFlags.StringLiteral ? stringType :
type.flags & TypeFlags.StringLikeLiteral ? stringType :
type.flags & TypeFlags.NumberLiteral ? numberType :
type.flags & TypeFlags.BigIntLiteral ? bigintType :
type.flags & TypeFlags.BooleanLiteral ? booleanType :
Expand All @@ -19191,7 +19197,7 @@ namespace ts {

function getWidenedLiteralType(type: Type): Type {
return type.flags & TypeFlags.EnumLiteral && isFreshLiteralType(type) ? getBaseTypeOfEnumLiteralType(<LiteralType>type) :
type.flags & TypeFlags.StringLiteral && isFreshLiteralType(type) ? stringType :
type.flags & TypeFlags.StringLikeLiteral && isFreshLiteralType(type) ? stringType :
type.flags & TypeFlags.NumberLiteral && isFreshLiteralType(type) ? numberType :
type.flags & TypeFlags.BigIntLiteral && isFreshLiteralType(type) ? bigintType :
type.flags & TypeFlags.BooleanLiteral && isFreshLiteralType(type) ? booleanType :
Expand Down Expand Up @@ -20692,7 +20698,7 @@ namespace ts {
}

function isTypeOrBaseIdenticalTo(s: Type, t: Type) {
return isTypeIdenticalTo(s, t) || !!(t.flags & TypeFlags.String && s.flags & TypeFlags.StringLiteral || t.flags & TypeFlags.Number && s.flags & TypeFlags.NumberLiteral);
return isTypeIdenticalTo(s, t) || !!(t.flags & TypeFlags.String && s.flags & TypeFlags.StringLikeLiteral || t.flags & TypeFlags.Number && s.flags & TypeFlags.NumberLiteral);
}

function isTypeCloselyMatchedBy(s: Type, t: Type) {
Expand Down Expand Up @@ -30837,7 +30843,7 @@ namespace ts {
texts.push(span.literal.text);
types.push(isTypeAssignableTo(type, templateConstraintType) ? type : stringType);
}
return isConstContext(node) ? getTemplateLiteralType(texts, types) : stringType;
return getFreshTypeOfLiteralType(getTemplateLiteralType(texts, types));
}

function getContextNode(node: Expression): Node {
Expand All @@ -30858,7 +30864,7 @@ namespace ts {
// We strip literal freshness when an appropriate contextual type is present such that contextually typed
// literals always preserve their literal types (otherwise they might widen during type inference). An alternative
// here would be to not mark contextually typed literals as fresh in the first place.
const result = maybeTypeOfKind(type, TypeFlags.Literal) && isLiteralOfContextualType(type, instantiateContextualType(contextualType, node)) ?
const result = maybeTypeOfKind(type, TypeFlags.FreshableLiteral) && isLiteralOfContextualType(type, instantiateContextualType(contextualType, node)) ?
getRegularTypeOfLiteralType(type) : type;
return result;
}
Expand Down Expand Up @@ -30948,15 +30954,15 @@ namespace ts {
// this a literal context for literals of that primitive type. For example, given a
// type parameter 'T extends string', infer string literal types for T.
const constraint = getBaseConstraintOfType(contextualType) || unknownType;
return maybeTypeOfKind(constraint, TypeFlags.String) && maybeTypeOfKind(candidateType, TypeFlags.StringLiteral) ||
return maybeTypeOfKind(constraint, TypeFlags.String) && maybeTypeOfKind(candidateType, TypeFlags.StringLikeLiteral) ||
maybeTypeOfKind(constraint, TypeFlags.Number) && maybeTypeOfKind(candidateType, TypeFlags.NumberLiteral) ||
maybeTypeOfKind(constraint, TypeFlags.BigInt) && maybeTypeOfKind(candidateType, TypeFlags.BigIntLiteral) ||
maybeTypeOfKind(constraint, TypeFlags.ESSymbol) && maybeTypeOfKind(candidateType, TypeFlags.UniqueESSymbol) ||
isLiteralOfContextualType(candidateType, constraint);
}
// If the contextual type is a literal of a particular primitive type, we consider this a
// literal context for all literals of that primitive type.
return !!(contextualType.flags & (TypeFlags.StringLiteral | TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) && maybeTypeOfKind(candidateType, TypeFlags.StringLiteral) ||
return !!(contextualType.flags & (TypeFlags.StringLikeLiteral | TypeFlags.Index | TypeFlags.StringMapping) && maybeTypeOfKind(candidateType, TypeFlags.StringLikeLiteral) ||
contextualType.flags & TypeFlags.NumberLiteral && maybeTypeOfKind(candidateType, TypeFlags.NumberLiteral) ||
contextualType.flags & TypeFlags.BigIntLiteral && maybeTypeOfKind(candidateType, TypeFlags.BigIntLiteral) ||
contextualType.flags & TypeFlags.BooleanLiteral && maybeTypeOfKind(candidateType, TypeFlags.BooleanLiteral) ||
Expand Down Expand Up @@ -38482,7 +38488,8 @@ namespace ts {

function isLiteralConstDeclaration(node: VariableDeclaration | PropertyDeclaration | PropertySignature | ParameterDeclaration): boolean {
if (isDeclarationReadonly(node) || isVariableDeclaration(node) && isVarConst(node)) {
return isFreshLiteralType(getTypeOfSymbol(getSymbolOfNode(node)));
const type = getTypeOfSymbol(getSymbolOfNode(node));
return !!(type.flags & TypeFlags.Literal) && isFreshLiteralType(type);
}
return false;
}
Expand Down
10 changes: 9 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4970,6 +4970,10 @@ namespace ts {
Unit = Literal | UniqueESSymbol | Nullable,
StringOrNumberLiteral = StringLiteral | NumberLiteral,
/* @internal */
StringLikeLiteral = StringLiteral | TemplateLiteral,
/* @internal */
FreshableLiteral = Literal | TemplateLiteral,
/* @internal */
StringOrNumberLiteralOrUnique = StringLiteral | NumberLiteral | UniqueESSymbol,
/* @internal */
DefinitelyFalsy = StringLiteral | NumberLiteral | BigIntLiteral | BooleanLiteral | Void | Undefined | Null,
Expand Down Expand Up @@ -5063,7 +5067,9 @@ namespace ts {
}

/* @internal */
export type FreshableType = LiteralType | FreshableIntrinsicType;
export type FreshableLiteralType = LiteralType | TemplateLiteralType;
/* @internal */
export type FreshableType = FreshableLiteralType | FreshableIntrinsicType;

// String literal types (TypeFlags.StringLiteral)
// Numeric literal types (TypeFlags.NumberLiteral)
Expand Down Expand Up @@ -5461,6 +5467,8 @@ namespace ts {
export interface TemplateLiteralType extends InstantiableType {
texts: readonly string[]; // Always one element longer than types
types: readonly Type[]; // Always at least one element
freshType: TemplateLiteralType; // Fresh version of type
regularType: TemplateLiteralType; // Regular version of type
}

export interface StringMappingType extends InstantiableType {
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/TemplateExpression1.types
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
=== tests/cases/conformance/es6/templates/TemplateExpression1.ts ===
var v = `foo ${ a
>v : string
>`foo ${ a : string
>`foo ${ a : `foo ${any}`
>a : any

2 changes: 1 addition & 1 deletion tests/baselines/reference/accessorsOverrideProperty2.types
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Derived extends Base {
>console.log : (...data: any[]) => void
>console : Console
>log : (...data: any[]) => void
>`x was set to ${value}` : string
>`x was set to ${value}` : `x was set to ${number}`
>value : number
}

Expand Down
2 changes: 2 additions & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2653,6 +2653,8 @@ declare namespace ts {
export interface TemplateLiteralType extends InstantiableType {
texts: readonly string[];
types: readonly Type[];
freshType: TemplateLiteralType;
regularType: TemplateLiteralType;
}
export interface StringMappingType extends InstantiableType {
symbol: Symbol;
Expand Down
2 changes: 2 additions & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2653,6 +2653,8 @@ declare namespace ts {
export interface TemplateLiteralType extends InstantiableType {
texts: readonly string[];
types: readonly Type[];
freshType: TemplateLiteralType;
regularType: TemplateLiteralType;
}
export interface StringMappingType extends InstantiableType {
symbol: Symbol;
Expand Down
12 changes: 6 additions & 6 deletions tests/baselines/reference/asOperator3.types
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,23 @@ declare function tag(...x: any[]): any;

var a = `${123 + 456 as number}`;
>a : string
>`${123 + 456 as number}` : string
>`${123 + 456 as number}` : `${number}`
>123 + 456 as number : number
>123 + 456 : number
>123 : 123
>456 : 456

var b = `leading ${123 + 456 as number}`;
>b : string
>`leading ${123 + 456 as number}` : string
>`leading ${123 + 456 as number}` : `leading ${number}`
>123 + 456 as number : number
>123 + 456 : number
>123 : 123
>456 : 456

var c = `${123 + 456 as number} trailing`;
>c : string
>`${123 + 456 as number} trailing` : string
>`${123 + 456 as number} trailing` : `${number} trailing`
>123 + 456 as number : number
>123 + 456 : number
>123 : 123
Expand All @@ -30,7 +30,7 @@ var c = `${123 + 456 as number} trailing`;
var d = `Hello ${123} World` as string;
>d : string
>`Hello ${123} World` as string : string
>`Hello ${123} World` : string
>`Hello ${123} World` : "Hello 123 World"
>123 : 123

var e = `Hello` as string;
Expand All @@ -43,15 +43,15 @@ var f = 1 + `${1} end of string` as string;
>1 + `${1} end of string` as string : string
>1 + `${1} end of string` : string
>1 : 1
>`${1} end of string` : string
>`${1} end of string` : "1 end of string"
>1 : 1

var g = tag `Hello ${123} World` as string;
>g : string
>tag `Hello ${123} World` as string : string
>tag `Hello ${123} World` : any
>tag : (...x: any[]) => any
>`Hello ${123} World` : string
>`Hello ${123} World` : "Hello 123 World"
>123 : 123

var h = tag `Hello` as string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ let n = Math.random();

let s = `${n}`;
>s : string
>`${n}` : string
>`${n}` : `${number}`
>n : number

const numericIndex = { [n]: 1 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@ var v = {

[`hello ${a} bye`]() { }
>[`hello ${a} bye`] : () => void
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@ var v = {

[`hello ${a} bye`]() { }
>[`hello ${a} bye`] : () => void
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ var v = {

get [`hello ${a} bye`]() { return 0; }
>[`hello ${a} bye`] : number
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
>0 : 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ var v = {

get [`hello ${a} bye`]() { return 0; }
>[`hello ${a} bye`] : number
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
>0 : 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class C {

static [`hello ${a} bye`] = 0
>[`hello ${a} bye`] : number
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
>0 : 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class C {

static [`hello ${a} bye`] = 0
>[`hello ${a} bye`] : number
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
>0 : 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@ class C {

static [`hello ${a} bye`]() { }
>[`hello ${a} bye`] : () => void
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@ class C {

static [`hello ${a} bye`]() { }
>[`hello ${a} bye`] : () => void
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class C {

get [`hello ${a} bye`]() { return 0; }
>[`hello ${a} bye`] : number
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
>0 : 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class C {

get [`hello ${a} bye`]() { return 0; }
>[`hello ${a} bye`] : number
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
>0 : 0
}
2 changes: 1 addition & 1 deletion tests/baselines/reference/computedPropertyNames4_ES5.types
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ var v = {

[`hello ${a} bye`]: 0
>[`hello ${a} bye`] : number
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
>0 : 0
}
2 changes: 1 addition & 1 deletion tests/baselines/reference/computedPropertyNames4_ES6.types
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ var v = {

[`hello ${a} bye`]: 0
>[`hello ${a} bye`] : number
>`hello ${a} bye` : string
>`hello ${a} bye` : `hello ${any} bye`
>a : any
>0 : 0
}
Loading

0 comments on commit ee1f262

Please sign in to comment.