Fix issues + Support template literal types as discriminants (#46137)

* Fix issues + Support template literal types in discriminants

* Add tests

* Address CR feedback
This commit is contained in:
Anders Hejlsberg
2021-09-29 17:38:43 -07:00
committed by GitHub
parent f09c5e80c6
commit 5ec836db82
6 changed files with 448 additions and 15 deletions

View File

@@ -12040,7 +12040,7 @@ namespace ts {
else if (type !== firstType) {
checkFlags |= CheckFlags.HasNonUniformType;
}
if (isLiteralType(type)) {
if (isLiteralType(type) || isPatternLiteralType(type)) {
checkFlags |= CheckFlags.HasLiteralType;
}
if (type.flags & TypeFlags.Never) {
@@ -19014,6 +19014,9 @@ namespace ts {
}
else if (target.flags & TypeFlags.TemplateLiteral) {
if (source.flags & TypeFlags.TemplateLiteral) {
if (relation === comparableRelation) {
return templateLiteralTypesDefinitelyUnrelated(source as TemplateLiteralType, target as TemplateLiteralType) ? Ternary.False : Ternary.True;
}
// Report unreliable variance for type variables referenced in template literal type placeholders.
// For example, `foo-${number}` is related to `foo-${string}` even though number isn't related to string.
instantiateType(source, makeFunctionTypeMapper(reportUnreliableMarkers));
@@ -19053,11 +19056,10 @@ namespace ts {
return result;
}
}
else if (source.flags & TypeFlags.TemplateLiteral) {
else if (source.flags & TypeFlags.TemplateLiteral && !(target.flags & TypeFlags.Object)) {
if (!(target.flags & TypeFlags.TemplateLiteral)) {
const baseConstraint = getBaseConstraintOfType(source);
const constraint = baseConstraint && baseConstraint !== source ? baseConstraint : stringType;
if (result = isRelatedTo(constraint, target, reportErrors)) {
const constraint = getBaseConstraintOfType(source);
if (constraint && constraint !== source && (result = isRelatedTo(constraint, target, reportErrors))) {
resetErrorInfo(saveErrorInfo);
return result;
}
@@ -21428,6 +21430,18 @@ namespace ts {
return !!(type.symbol && some(type.symbol.declarations, hasSkipDirectInferenceFlag));
}
function templateLiteralTypesDefinitelyUnrelated(source: TemplateLiteralType, target: TemplateLiteralType) {
// Two template literal types with diffences in their starting or ending text spans are definitely unrelated.
const sourceStart = source.texts[0];
const targetStart = target.texts[0];
const sourceEnd = source.texts[source.texts.length - 1];
const targetEnd = target.texts[target.texts.length - 1];
const startLen = Math.min(sourceStart.length, targetStart.length);
const endLen = Math.min(sourceEnd.length, targetEnd.length);
return sourceStart.slice(0, startLen) !== targetStart.slice(0, startLen) ||
sourceEnd.slice(sourceEnd.length - endLen) !== targetEnd.slice(targetEnd.length - endLen);
}
function isValidBigIntString(s: string): boolean {
const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false);
let success = true;
@@ -22528,7 +22542,7 @@ namespace ts {
if ((prop as TransientSymbol).isDiscriminantProperty === undefined) {
(prop as TransientSymbol).isDiscriminantProperty =
((prop as TransientSymbol).checkFlags & CheckFlags.Discriminant) === CheckFlags.Discriminant &&
!maybeTypeOfKind(getTypeOfSymbol(prop), TypeFlags.Instantiable & ~TypeFlags.TemplateLiteral);
!isGenericType(getTypeOfSymbol(prop));
}
return !!(prop as TransientSymbol).isDiscriminantProperty;
}
@@ -23087,15 +23101,17 @@ namespace ts {
return filterType(type, t => (t.flags & kind) !== 0);
}
// Return a new type in which occurrences of the string and number primitive types in
// typeWithPrimitives have been replaced with occurrences of string literals and numeric
// literals in typeWithLiterals, respectively.
// Return a new type in which occurrences of the string, number and bigint primitives and placeholder template
// literal types in typeWithPrimitives have been replaced with occurrences of compatible and more specific types
// from typeWithLiterals. This is essentially a limited form of intersection between the two types. We avoid a
// true intersection because it is more costly and, when applied to union types, generates a large number of
// types we don't actually care about.
function replacePrimitivesWithLiterals(typeWithPrimitives: Type, typeWithLiterals: Type) {
if (isTypeSubsetOf(stringType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.StringLiteral) ||
isTypeSubsetOf(numberType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.NumberLiteral) ||
isTypeSubsetOf(bigintType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.BigIntLiteral)) {
if (maybeTypeOfKind(typeWithPrimitives, TypeFlags.String | TypeFlags.TemplateLiteral | TypeFlags.Number | TypeFlags.BigInt) &&
maybeTypeOfKind(typeWithLiterals, TypeFlags.StringLiteral | TypeFlags.TemplateLiteral | TypeFlags.StringMapping | TypeFlags.NumberLiteral | TypeFlags.BigIntLiteral)) {
return mapType(typeWithPrimitives, t =>
t.flags & TypeFlags.String ? extractTypesOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.StringLiteral) :
t.flags & TypeFlags.String ? extractTypesOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.StringLiteral | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) :
isPatternLiteralType(t) && !maybeTypeOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.TemplateLiteral | TypeFlags.StringMapping) ? extractTypesOfKind(typeWithLiterals, TypeFlags.StringLiteral) :
t.flags & TypeFlags.Number ? extractTypesOfKind(typeWithLiterals, TypeFlags.Number | TypeFlags.NumberLiteral) :
t.flags & TypeFlags.BigInt ? extractTypesOfKind(typeWithLiterals, TypeFlags.BigInt | TypeFlags.BigIntLiteral) : t);
}
@@ -23938,7 +23954,7 @@ namespace ts {
const narrowedPropType = narrowType(propType);
return filterType(type, t => {
const discriminantType = getTypeOfPropertyOrIndexSignature(t, propName);
return !(discriminantType.flags & TypeFlags.Never) && isTypeComparableTo(discriminantType, narrowedPropType);
return !(narrowedPropType.flags & TypeFlags.Never) && isTypeComparableTo(narrowedPropType, discriminantType);
});
}

View File

@@ -5,9 +5,11 @@ tests/cases/conformance/types/literal/templateLiteralTypes3.ts(71,5): error TS23
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(72,5): error TS2322: Type '`*${string}*`' is not assignable to type '`*${number}*`'.
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(74,5): error TS2322: Type '"*false*" | "*true*"' is not assignable to type '`*${number}*`'.
Type '"*false*"' is not assignable to type '`*${number}*`'.
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(133,9): error TS2367: This condition will always return 'false' since the types '`foo-${string}`' and '`baz-${string}`' have no overlap.
tests/cases/conformance/types/literal/templateLiteralTypes3.ts(141,9): error TS2367: This condition will always return 'false' since the types '`foo-${T}`' and '`baz-${T}`' have no overlap.
==== tests/cases/conformance/types/literal/templateLiteralTypes3.ts (6 errors) ====
==== tests/cases/conformance/types/literal/templateLiteralTypes3.ts (8 errors) ====
// Inference from template literal type to template literal type
type Foo1<T> = T extends `*${infer U}*` ? U : never;
@@ -146,4 +148,54 @@ tests/cases/conformance/types/literal/templateLiteralTypes3.ts(74,5): error TS23
declare function chain<F extends keyof Schema>(field: F | `${F}.${F}`): void;
chain("a");
// Repro from #46125
function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) {
if (x === y) {
x; // `foo-${string}`
}
if (x === z) { // Error
~~~~~~~
!!! error TS2367: This condition will always return 'false' since the types '`foo-${string}`' and '`baz-${string}`' have no overlap.
}
}
function ff2<T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) {
if (x === y) {
x; // `foo-${T}`
}
if (x === z) { // Error
~~~~~~~
!!! error TS2367: This condition will always return 'false' since the types '`foo-${T}`' and '`baz-${T}`' have no overlap.
}
}
function ff3(x: string, y: `foo-${string}` | 'bar') {
if (x === y) {
x; // `foo-${string}` | 'bar'
}
}
function ff4(x: string, y: `foo-${string}`) {
if (x === 'foo-test') {
x; // 'foo-test'
}
if (y === 'foo-test') {
y; // 'foo-test'
}
}
// Repro from #46045
type Action =
| { type: `${string}_REQUEST` }
| { type: `${string}_SUCCESS`, response: string };
function reducer(action: Action) {
if (action.type === 'FOO_SUCCESS') {
action.type;
action.response;
}
}

View File

@@ -124,6 +124,52 @@ type Schema = { a: { b: { c: number } } };
declare function chain<F extends keyof Schema>(field: F | `${F}.${F}`): void;
chain("a");
// Repro from #46125
function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) {
if (x === y) {
x; // `foo-${string}`
}
if (x === z) { // Error
}
}
function ff2<T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) {
if (x === y) {
x; // `foo-${T}`
}
if (x === z) { // Error
}
}
function ff3(x: string, y: `foo-${string}` | 'bar') {
if (x === y) {
x; // `foo-${string}` | 'bar'
}
}
function ff4(x: string, y: `foo-${string}`) {
if (x === 'foo-test') {
x; // 'foo-test'
}
if (y === 'foo-test') {
y; // 'foo-test'
}
}
// Repro from #46045
type Action =
| { type: `${string}_REQUEST` }
| { type: `${string}_SUCCESS`, response: string };
function reducer(action: Action) {
if (action.type === 'FOO_SUCCESS') {
action.type;
action.response;
}
}
//// [templateLiteralTypes3.js]
@@ -177,6 +223,40 @@ var templated1 = value1 + " abc";
var value2 = "abc";
var templated2 = value2 + " abc";
chain("a");
// Repro from #46125
function ff1(x, y, z) {
if (x === y) {
x; // `foo-${string}`
}
if (x === z) { // Error
}
}
function ff2(x, y, z) {
if (x === y) {
x; // `foo-${T}`
}
if (x === z) { // Error
}
}
function ff3(x, y) {
if (x === y) {
x; // `foo-${string}` | 'bar'
}
}
function ff4(x, y) {
if (x === 'foo-test') {
x; // 'foo-test'
}
if (y === 'foo-test') {
y; // 'foo-test'
}
}
function reducer(action) {
if (action.type === 'FOO_SUCCESS') {
action.type;
action.response;
}
}
//// [templateLiteralTypes3.d.ts]
@@ -233,3 +313,14 @@ declare type Schema = {
};
};
declare function chain<F extends keyof Schema>(field: F | `${F}.${F}`): void;
declare function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`): void;
declare function ff2<T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`): void;
declare function ff3(x: string, y: `foo-${string}` | 'bar'): void;
declare function ff4(x: string, y: `foo-${string}`): void;
declare type Action = {
type: `${string}_REQUEST`;
} | {
type: `${string}_SUCCESS`;
response: string;
};
declare function reducer(action: Action): void;

View File

@@ -405,3 +405,114 @@ declare function chain<F extends keyof Schema>(field: F | `${F}.${F}`): void;
chain("a");
>chain : Symbol(chain, Decl(templateLiteralTypes3.ts, 120, 42))
// Repro from #46125
function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) {
>ff1 : Symbol(ff1, Decl(templateLiteralTypes3.ts, 124, 11))
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13))
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 128, 32))
>z : Symbol(z, Decl(templateLiteralTypes3.ts, 128, 52))
if (x === y) {
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13))
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 128, 32))
x; // `foo-${string}`
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13))
}
if (x === z) { // Error
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 128, 13))
>z : Symbol(z, Decl(templateLiteralTypes3.ts, 128, 52))
}
}
function ff2<T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) {
>ff2 : Symbol(ff2, Decl(templateLiteralTypes3.ts, 134, 1))
>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13))
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31))
>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13))
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 136, 45))
>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13))
>z : Symbol(z, Decl(templateLiteralTypes3.ts, 136, 60))
>T : Symbol(T, Decl(templateLiteralTypes3.ts, 136, 13))
if (x === y) {
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31))
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 136, 45))
x; // `foo-${T}`
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31))
}
if (x === z) { // Error
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 136, 31))
>z : Symbol(z, Decl(templateLiteralTypes3.ts, 136, 60))
}
}
function ff3(x: string, y: `foo-${string}` | 'bar') {
>ff3 : Symbol(ff3, Decl(templateLiteralTypes3.ts, 142, 1))
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 144, 13))
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 144, 23))
if (x === y) {
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 144, 13))
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 144, 23))
x; // `foo-${string}` | 'bar'
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 144, 13))
}
}
function ff4(x: string, y: `foo-${string}`) {
>ff4 : Symbol(ff4, Decl(templateLiteralTypes3.ts, 148, 1))
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 150, 13))
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 150, 23))
if (x === 'foo-test') {
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 150, 13))
x; // 'foo-test'
>x : Symbol(x, Decl(templateLiteralTypes3.ts, 150, 13))
}
if (y === 'foo-test') {
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 150, 23))
y; // 'foo-test'
>y : Symbol(y, Decl(templateLiteralTypes3.ts, 150, 23))
}
}
// Repro from #46045
type Action =
>Action : Symbol(Action, Decl(templateLiteralTypes3.ts, 157, 1))
| { type: `${string}_REQUEST` }
>type : Symbol(type, Decl(templateLiteralTypes3.ts, 162, 7))
| { type: `${string}_SUCCESS`, response: string };
>type : Symbol(type, Decl(templateLiteralTypes3.ts, 163, 7))
>response : Symbol(response, Decl(templateLiteralTypes3.ts, 163, 34))
function reducer(action: Action) {
>reducer : Symbol(reducer, Decl(templateLiteralTypes3.ts, 163, 54))
>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17))
>Action : Symbol(Action, Decl(templateLiteralTypes3.ts, 157, 1))
if (action.type === 'FOO_SUCCESS') {
>action.type : Symbol(type, Decl(templateLiteralTypes3.ts, 162, 7), Decl(templateLiteralTypes3.ts, 163, 7))
>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17))
>type : Symbol(type, Decl(templateLiteralTypes3.ts, 162, 7), Decl(templateLiteralTypes3.ts, 163, 7))
action.type;
>action.type : Symbol(type, Decl(templateLiteralTypes3.ts, 163, 7))
>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17))
>type : Symbol(type, Decl(templateLiteralTypes3.ts, 163, 7))
action.response;
>action.response : Symbol(response, Decl(templateLiteralTypes3.ts, 163, 34))
>action : Symbol(action, Decl(templateLiteralTypes3.ts, 165, 17))
>response : Symbol(response, Decl(templateLiteralTypes3.ts, 163, 34))
}
}

View File

@@ -396,3 +396,120 @@ chain("a");
>chain : <F extends "a">(field: F | `${F}.${F}`) => void
>"a" : "a"
// Repro from #46125
function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) {
>ff1 : (x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) => void
>x : `foo-${string}`
>y : `${string}-bar`
>z : `baz-${string}`
if (x === y) {
>x === y : boolean
>x : `foo-${string}`
>y : `${string}-bar`
x; // `foo-${string}`
>x : `foo-${string}`
}
if (x === z) { // Error
>x === z : boolean
>x : `foo-${string}`
>z : `baz-${string}`
}
}
function ff2<T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) {
>ff2 : <T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) => void
>x : `foo-${T}`
>y : `${T}-bar`
>z : `baz-${T}`
if (x === y) {
>x === y : boolean
>x : `foo-${T}`
>y : `${T}-bar`
x; // `foo-${T}`
>x : `foo-${T}`
}
if (x === z) { // Error
>x === z : boolean
>x : `foo-${T}`
>z : `baz-${T}`
}
}
function ff3(x: string, y: `foo-${string}` | 'bar') {
>ff3 : (x: string, y: `foo-${string}` | 'bar') => void
>x : string
>y : "bar" | `foo-${string}`
if (x === y) {
>x === y : boolean
>x : string
>y : "bar" | `foo-${string}`
x; // `foo-${string}` | 'bar'
>x : "bar" | `foo-${string}`
}
}
function ff4(x: string, y: `foo-${string}`) {
>ff4 : (x: string, y: `foo-${string}`) => void
>x : string
>y : `foo-${string}`
if (x === 'foo-test') {
>x === 'foo-test' : boolean
>x : string
>'foo-test' : "foo-test"
x; // 'foo-test'
>x : "foo-test"
}
if (y === 'foo-test') {
>y === 'foo-test' : boolean
>y : `foo-${string}`
>'foo-test' : "foo-test"
y; // 'foo-test'
>y : "foo-test"
}
}
// Repro from #46045
type Action =
>Action : Action
| { type: `${string}_REQUEST` }
>type : `${string}_REQUEST`
| { type: `${string}_SUCCESS`, response: string };
>type : `${string}_SUCCESS`
>response : string
function reducer(action: Action) {
>reducer : (action: Action) => void
>action : Action
if (action.type === 'FOO_SUCCESS') {
>action.type === 'FOO_SUCCESS' : boolean
>action.type : `${string}_REQUEST` | `${string}_SUCCESS`
>action : Action
>type : `${string}_REQUEST` | `${string}_SUCCESS`
>'FOO_SUCCESS' : "FOO_SUCCESS"
action.type;
>action.type : "FOO_SUCCESS"
>action : { type: `${string}_SUCCESS`; response: string; }
>type : "FOO_SUCCESS"
action.response;
>action.response : string
>action : { type: `${string}_SUCCESS`; response: string; }
>response : string
}
}

View File

@@ -126,3 +126,49 @@ type Schema = { a: { b: { c: number } } };
declare function chain<F extends keyof Schema>(field: F | `${F}.${F}`): void;
chain("a");
// Repro from #46125
function ff1(x: `foo-${string}`, y: `${string}-bar`, z: `baz-${string}`) {
if (x === y) {
x; // `foo-${string}`
}
if (x === z) { // Error
}
}
function ff2<T extends string>(x: `foo-${T}`, y: `${T}-bar`, z: `baz-${T}`) {
if (x === y) {
x; // `foo-${T}`
}
if (x === z) { // Error
}
}
function ff3(x: string, y: `foo-${string}` | 'bar') {
if (x === y) {
x; // `foo-${string}` | 'bar'
}
}
function ff4(x: string, y: `foo-${string}`) {
if (x === 'foo-test') {
x; // 'foo-test'
}
if (y === 'foo-test') {
y; // 'foo-test'
}
}
// Repro from #46045
type Action =
| { type: `${string}_REQUEST` }
| { type: `${string}_SUCCESS`, response: string };
function reducer(action: Action) {
if (action.type === 'FOO_SUCCESS') {
action.type;
action.response;
}
}