Skip to content

Commit

Permalink
Allow pattern literal types like http://${string} to exist and be r…
Browse files Browse the repository at this point in the history
…easoned about (#40598)

* Allow pattern literal types like `http://${string}` to exist and be reasoned about

* Allow bigint, number, null, and undefined in template holes

* Add test of the trivia case

* Handle `any` in template holes, add assignability rules for template -> template relations

* Explicitly test concatenated patterns

* PR Feedback
  • Loading branch information
weswigham committed Sep 23, 2020
1 parent a91c287 commit a960463
Show file tree
Hide file tree
Showing 10 changed files with 1,856 additions and 41 deletions.
154 changes: 122 additions & 32 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ namespace ts {
const stringNumberSymbolType = getUnionType([stringType, numberType, esSymbolType]);
const keyofConstraintType = keyofStringsOnly ? stringType : stringNumberSymbolType;
const numberOrBigIntType = getUnionType([numberType, bigintType]);
const templateConstraintType = getUnionType([stringType, numberType, booleanType, bigintType]);
const templateConstraintType = getUnionType([stringType, numberType, booleanType, bigintType, nullType, undefinedType]) as UnionType;

const restrictiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? getRestrictiveTypeParameter(<TypeParameter>t) : t);
const permissiveMapper: TypeMapper = makeFunctionTypeMapper(t => t.flags & TypeFlags.TypeParameter ? wildcardType : t);
Expand Down Expand Up @@ -13207,6 +13207,30 @@ namespace ts {
return true;
}

/**
* Returns `true` if the intersection of the template literals and string literals is the empty set, eg `get${string}` & "setX", and should reduce to `never`
*/
function extractRedundantTemplateLiterals(types: Type[]): boolean {
let i = types.length;
const literals = filter(types, t => !!(t.flags & TypeFlags.StringLiteral));
while (i > 0) {
i--;
const t = types[i];
if (!(t.flags & TypeFlags.TemplateLiteral)) continue;
for (const t2 of literals) {
if (isTypeSubtypeOf(t2, t)) {
// eg, ``get${T}` & "getX"` is just `"getX"`
orderedRemoveItemAt(types, i);
break;
}
else if (isPatternLiteralType(t)) {
return true;
}
}
}
return false;
}

function extractIrreducible(types: Type[], flag: TypeFlags) {
if (every(types, t => !!(t.flags & TypeFlags.Union) && some((t as UnionType).types, tt => !!(tt.flags & flag)))) {
for (let i = 0; i < types.length; i++) {
Expand Down Expand Up @@ -13355,7 +13379,12 @@ namespace ts {
}
}
else {
result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
if (includes & TypeFlags.TemplateLiteral && includes & TypeFlags.StringLiteral && extractRedundantTemplateLiterals(typeSet)) {
result = neverType;
}
else {
result = createIntersectionType(typeSet, aliasSymbol, aliasTypeArguments);
}
}
intersectionTypes.set(id, result);
}
Expand Down Expand Up @@ -13531,7 +13560,7 @@ namespace ts {
function addSpans(texts: readonly string[], types: readonly Type[]): boolean {
for (let i = 0; i < types.length; i++) {
const t = types[i];
if (t.flags & TypeFlags.Literal) {
if (t.flags & (TypeFlags.Literal | TypeFlags.Null | TypeFlags.Undefined)) {
text += getTemplateStringForType(t) || "";
text += texts[i + 1];
}
Expand All @@ -13540,7 +13569,7 @@ namespace ts {
if (!addSpans((<TemplateLiteralType>t).texts, (<TemplateLiteralType>t).types)) return false;
text += texts[i + 1];
}
else if (isGenericIndexType(t)) {
else if (isGenericIndexType(t) || isPatternLiteralPlaceholderType(t)) {
newTypes.push(t);
newTexts.push(text);
text = texts[i + 1];
Expand All @@ -13558,6 +13587,8 @@ namespace ts {
type.flags & TypeFlags.NumberLiteral ? "" + (<NumberLiteralType>type).value :
type.flags & TypeFlags.BigIntLiteral ? pseudoBigIntToString((<BigIntLiteralType>type).value) :
type.flags & TypeFlags.BooleanLiteral ? (<IntrinsicType>type).intrinsicName :
type.flags & TypeFlags.Null ? "null" :
type.flags & TypeFlags.Undefined ? "undefined" :
undefined;
}

Expand Down Expand Up @@ -13817,6 +13848,14 @@ namespace ts {
accessNode;
}

function isPatternLiteralPlaceholderType(type: Type) {
return templateConstraintType.types.indexOf(type) !== -1 || !!(type.flags & TypeFlags.Any);
}

function isPatternLiteralType(type: Type) {
return !!(type.flags & TypeFlags.TemplateLiteral) && every((type as TemplateLiteralType).types, isPatternLiteralPlaceholderType);
}

function isGenericObjectType(type: Type): boolean {
if (type.flags & TypeFlags.UnionOrIntersection) {
if (!((<UnionOrIntersectionType>type).objectFlags & ObjectFlags.IsGenericObjectTypeComputed)) {
Expand All @@ -13836,7 +13875,7 @@ namespace ts {
}
return !!((<UnionOrIntersectionType>type).objectFlags & ObjectFlags.IsGenericIndexType);
}
return !!(type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping));
return !!(type.flags & (TypeFlags.InstantiableNonPrimitive | TypeFlags.Index | TypeFlags.TemplateLiteral | TypeFlags.StringMapping)) && !isPatternLiteralType(type);
}

function isThisTypeParameter(type: Type): boolean {
Expand Down Expand Up @@ -14562,6 +14601,8 @@ namespace ts {
return !!(type.flags & TypeFlags.Literal) && (<LiteralType>type).freshType === type;
}

function getLiteralType(value: string): StringLiteralType;
function getLiteralType(value: string | number | PseudoBigInt, enumId?: number, symbol?: Symbol): LiteralType;
function getLiteralType(value: string | number | PseudoBigInt, enumId?: number, symbol?: Symbol) {
// We store all literal types in a single map with keys of the form '#NNN' and '@SSS',
// where NNN is the text representation of a numeric literal and SSS are the characters
Expand Down Expand Up @@ -17346,6 +17387,15 @@ namespace ts {
}
}
}
else if (target.flags & TypeFlags.TemplateLiteral && source.flags & TypeFlags.StringLiteral) {
if (isPatternLiteralType(target)) {
// match all non-`string` segments
const result = inferLiteralsFromTemplateLiteralType(source as StringLiteralType, target as TemplateLiteralType);
if (result && every(result, (r, i) => isStringLiteralTypeValueParsableAsType(r, (target as TemplateLiteralType).types[i]))) {
return Ternary.True;
}
}
}

if (source.flags & TypeFlags.TypeVariable) {
if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) {
Expand Down Expand Up @@ -17386,8 +17436,15 @@ 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;
}
const constraint = getBaseConstraintOfType(source);
if (constraint && (result = isRelatedTo(constraint, target, reportErrors))) {
if (constraint && constraint !== source && (result = isRelatedTo(constraint, target, reportErrors))) {
resetErrorInfo(saveErrorInfo);
return result;
}
Expand Down Expand Up @@ -18308,12 +18365,12 @@ namespace ts {

if (type.flags & TypeFlags.Instantiable) {
const constraint = getConstraintOfType(type);
if (constraint) {
if (constraint && constraint !== type) {
return typeCouldHaveTopLevelSingletonTypes(constraint);
}
}

return isUnitType(type);
return isUnitType(type) || !!(type.flags & TypeFlags.TemplateLiteral);
}

function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) {
Expand Down Expand Up @@ -19693,6 +19750,63 @@ namespace ts {
return !!(type.symbol && some(type.symbol.declarations, hasSkipDirectInferenceFlag));
}

function isValidBigIntString(s: string): boolean {
const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false);
let success = true;
scanner.setOnError(() => success = false);
scanner.setText(s + "n");
let result = scanner.scan();
if (result === SyntaxKind.MinusToken) {
result = scanner.scan();
}
const flags = scanner.getTokenFlags();
// validate that
// * scanning proceeded without error
// * a bigint can be scanned, and that when it is scanned, it is
// * the full length of the input string (so the scanner is one character beyond the augmented input length)
// * it does not contain a numeric seperator (the `BigInt` constructor does not accept a numeric seperator in its input)
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator);
}

function isStringLiteralTypeValueParsableAsType(s: StringLiteralType, target: Type): boolean {
if (target.flags & TypeFlags.Union) {
return !!forEachType(target, t => isStringLiteralTypeValueParsableAsType(s, t));
}
switch (target) {
case stringType: return true;
case numberType: return s.value !== "" && isFinite(+(s.value));
case bigintType: return s.value !== "" && isValidBigIntString(s.value);
// the next 4 should be handled in `getTemplateLiteralType`, as they are all exactly one value, but are here for completeness, just in case
// this function is ever used on types which don't come from template literal holes
case trueType: return s.value === "true";
case falseType: return s.value === "false";
case undefinedType: return s.value === "undefined";
case nullType: return s.value === "null";
default: return !!(target.flags & TypeFlags.Any);
}
}

function inferLiteralsFromTemplateLiteralType(source: StringLiteralType, target: TemplateLiteralType): StringLiteralType[] | undefined {
const value = source.value;
const texts = target.texts;
const lastIndex = texts.length - 1;
const startText = texts[0];
const endText = texts[lastIndex];
if (!(value.startsWith(startText) && value.endsWith(endText))) return undefined;
const matches = [];
const str = value.slice(startText.length, value.length - endText.length);
let pos = 0;
for (let i = 1; i < lastIndex; i++) {
const delim = texts[i];
const delimPos = delim.length > 0 ? str.indexOf(delim, pos) : pos < str.length ? pos + 1 : -1;
if (delimPos < 0) return undefined;
matches.push(getLiteralType(str.slice(pos, delimPos)));
pos = delimPos + delim.length;
}
matches.push(getLiteralType(str.slice(pos)));
return matches;
}

function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority: InferencePriority = 0, contravariant = false) {
let bivariant = false;
let propagationType: Type;
Expand Down Expand Up @@ -20170,27 +20284,6 @@ namespace ts {
}
}

function inferLiteralsFromTemplateLiteralType(source: StringLiteralType, target: TemplateLiteralType): Type[] | undefined {
const value = source.value;
const texts = target.texts;
const lastIndex = texts.length - 1;
const startText = texts[0];
const endText = texts[lastIndex];
if (!(value.startsWith(startText) && value.endsWith(endText))) return undefined;
const matches = [];
const str = value.slice(startText.length, value.length - endText.length);
let pos = 0;
for (let i = 1; i < lastIndex; i++) {
const delim = texts[i];
const delimPos = delim.length > 0 ? str.indexOf(delim, pos) : pos < str.length ? pos + 1 : -1;
if (delimPos < 0) return undefined;
matches.push(getLiteralType(str.slice(pos, delimPos)));
pos = delimPos + delim.length;
}
matches.push(getLiteralType(str.slice(pos)));
return matches;
}

function inferFromObjectTypes(source: Type, target: Type) {
if (getObjectFlags(source) & ObjectFlags.Reference && getObjectFlags(target) & ObjectFlags.Reference && (
(<TypeReference>source).target === (<TypeReference>target).target || isArrayType(source) && isArrayType(target))) {
Expand Down Expand Up @@ -31688,9 +31781,6 @@ namespace ts {
checkSourceElement(span.type);
const type = getTypeFromTypeNode(span.type);
checkTypeAssignableTo(type, templateConstraintType, span.type);
if (!everyType(type, t => !!(t.flags & TypeFlags.Literal) || isGenericIndexType(t))) {
error(span.type, Diagnostics.Template_literal_type_argument_0_is_not_literal_type_or_a_generic_type, typeToString(type));
}
}
getTypeFromTypeNode(node);
}
Expand Down
5 changes: 1 addition & 4 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3039,10 +3039,7 @@
"category": "Error",
"code": 2792
},
"Template literal type argument '{0}' is not literal type or a generic type.": {
"category": "Error",
"code": 2793
},

"Expected {0} arguments, but got {1}. Did you forget to include 'void' in your type argument to 'Promise'?": {
"category": "Error",
"code": 2794
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4925,7 +4925,7 @@ namespace ts {
NotPrimitiveUnion = Any | Unknown | Enum | Void | Never | StructuredOrInstantiable,
// The following flags are aggregated during union and intersection type construction
/* @internal */
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive,
IncludesMask = Any | Unknown | Primitive | Never | Object | Union | Intersection | NonPrimitive | TemplateLiteral,
// The following flags are used for different purposes during union and intersection type construction
/* @internal */
IncludesStructuredOrInstantiable = TypeParameter,
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/constAssertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ declare function ff2<T extends string, U extends string>(x: T, y: U): `${T}-${U}
declare const ts1: "foo-bar";
declare const ts2: "foo-1" | "foo-0";
declare const ts3: "top-left" | "top-right" | "bottom-left" | "bottom-right";
declare function ff3(x: 'foo' | 'bar', y: object): string;
declare function ff3(x: 'foo' | 'bar', y: object): `foo${string}` | `bar${string}`;
declare type Action = "verify" | "write";
declare type ContentMatch = "match" | "nonMatch";
declare type Outcome = `${Action}_${ContentMatch}`;
Expand Down
6 changes: 3 additions & 3 deletions tests/baselines/reference/constAssertions.types
Original file line number Diff line number Diff line change
Expand Up @@ -441,13 +441,13 @@ const ts3 = ff2(!!true ? 'top' : 'bottom', !!true ? 'left' : 'right');
>'right' : "right"

function ff3(x: 'foo' | 'bar', y: object) {
>ff3 : (x: 'foo' | 'bar', y: object) => string
>ff3 : (x: 'foo' | 'bar', y: object) => `foo${string}` | `bar${string}`
>x : "foo" | "bar"
>y : object

return `${x}${y}` as const;
>`${x}${y}` as const : string
>`${x}${y}` : string
>`${x}${y}` as const : `foo${string}` | `bar${string}`
>`${x}${y}` : `foo${string}` | `bar${string}`
>x : "foo" | "bar"
>y : object
}
Expand Down

0 comments on commit a960463

Please sign in to comment.