Skip to content

Commit 9dfa74d

Browse files
feat(15048): One-sided Type Predicates
1 parent 8586c99 commit 9dfa74d

15 files changed

+972
-475
lines changed

src/compiler/checker.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,7 +1940,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
19401940
const markerSubTypeForCheck = createTypeParameter();
19411941
markerSubTypeForCheck.constraint = markerSuperTypeForCheck;
19421942

1943-
const noTypePredicate = createTypePredicate(TypePredicateKind.Identifier, "<<unresolved>>", 0, anyType);
1943+
const noTypePredicate = createTypePredicate(TypePredicateKind.Identifier, "<<unresolved>>", 0, anyType, /*strictSubtype*/ false);
19441944

19451945
const anySignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None);
19461946
const unknownSignature = createSignature(undefined, undefined, undefined, emptyArray, errorType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None);
@@ -7156,7 +7156,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
71567156
setEmitFlags(factory.createIdentifier(typePredicate.parameterName), EmitFlags.NoAsciiEscaping) :
71577157
factory.createThisTypeNode();
71587158
const typeNode = typePredicate.type && typeToTypeNodeHelper(typePredicate.type, context);
7159-
returnTypeNode = factory.createTypePredicateNode(assertsModifier, parameterName, typeNode);
7159+
const subtypeOfModifier = typePredicate.oneSided ? factory.createToken(SyntaxKind.SubtypeOfKeyword) : undefined;
7160+
returnTypeNode = factory.createTypePredicateNode(assertsModifier, parameterName, typeNode, subtypeOfModifier);
71607161
}
71617162
else {
71627163
const returnType = getReturnTypeOfSignature(signature);
@@ -9528,7 +9529,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
95289529
const predicate = factory.createTypePredicateNode(
95299530
typePredicate.kind === TypePredicateKind.AssertsThis || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? factory.createToken(SyntaxKind.AssertsKeyword) : undefined,
95309531
typePredicate.kind === TypePredicateKind.Identifier || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? factory.createIdentifier(typePredicate.parameterName) : factory.createThisTypeNode(),
9531-
typePredicate.type && nodeBuilder.typeToTypeNode(typePredicate.type, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName)! // TODO: GH#18217
9532+
typePredicate.type && nodeBuilder.typeToTypeNode(typePredicate.type, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName)!, // TODO: GH#18217
9533+
typePredicate.oneSided ? factory.createToken(SyntaxKind.SubtypeOfKeyword) : undefined
95329534
);
95339535
const printer = createPrinter({ removeComments: true });
95349536
const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration);
@@ -14083,8 +14085,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1408314085
return isPropertyDeclaration(node) && !hasAccessorModifier(node) && node.questionToken;
1408414086
}
1408514087

14086-
function createTypePredicate(kind: TypePredicateKind, parameterName: string | undefined, parameterIndex: number | undefined, type: Type | undefined): TypePredicate {
14087-
return { kind, parameterName, parameterIndex, type } as TypePredicate;
14088+
function createTypePredicate(kind: TypePredicateKind, parameterName: string | undefined, parameterIndex: number | undefined, type: Type | undefined, oneSided: boolean): TypePredicate {
14089+
return { kind, parameterName, parameterIndex, type, oneSided } as TypePredicate;
1408814090
}
1408914091

1409014092
/**
@@ -14408,9 +14410,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1440814410
const parameterName = node.parameterName;
1440914411
const type = node.type && getTypeFromTypeNode(node.type);
1441014412
return parameterName.kind === SyntaxKind.ThisType ?
14411-
createTypePredicate(node.assertsModifier ? TypePredicateKind.AssertsThis : TypePredicateKind.This, /*parameterName*/ undefined, /*parameterIndex*/ undefined, type) :
14413+
createTypePredicate(node.assertsModifier ? TypePredicateKind.AssertsThis : TypePredicateKind.This, /*parameterName*/ undefined, /*parameterIndex*/ undefined, type, !!node.subtypeOfModifier) :
1441214414
createTypePredicate(node.assertsModifier ? TypePredicateKind.AssertsIdentifier : TypePredicateKind.Identifier, parameterName.escapedText as string,
14413-
findIndex(signature.parameters, p => p.escapedName === parameterName.escapedText), type);
14415+
findIndex(signature.parameters, p => p.escapedName === parameterName.escapedText), type, !!node.subtypeOfModifier);
1441414416
}
1441514417

1441614418
function getUnionOrIntersectionType(types: Type[], kind: TypeFlags | undefined, unionReduction?: UnionReduction) {
@@ -16075,6 +16077,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1607516077
function getUnionOrIntersectionTypePredicate(signatures: readonly Signature[], kind: TypeFlags | undefined): TypePredicate | undefined {
1607616078
let first: TypePredicate | undefined;
1607716079
const types: Type[] = [];
16080+
let oneSided!: boolean;
1607816081
for (const sig of signatures) {
1607916082
const pred = getTypePredicateOfSignature(sig);
1608016083
if (!pred || pred.kind === TypePredicateKind.AssertsThis || pred.kind === TypePredicateKind.AssertsIdentifier) {
@@ -16091,9 +16094,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1609116094
// No common type predicate.
1609216095
return undefined;
1609316096
}
16097+
oneSided = kind === TypeFlags.Intersection ? oneSided || pred.oneSided : oneSided && pred.oneSided;
1609416098
}
1609516099
else {
1609616100
first = pred;
16101+
oneSided = pred.oneSided;
1609716102
}
1609816103
types.push(pred.type);
1609916104
}
@@ -16102,7 +16107,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1610216107
return undefined;
1610316108
}
1610416109
const compositeType = getUnionOrIntersectionType(types, kind);
16105-
return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, compositeType);
16110+
return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, compositeType, oneSided);
1610616111
}
1610716112

1610816113
function typePredicateKindsMatch(a: TypePredicate, b: TypePredicate): boolean {
@@ -18259,7 +18264,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1825918264
}
1826018265

1826118266
function instantiateTypePredicate(predicate: TypePredicate, mapper: TypeMapper): TypePredicate {
18262-
return createTypePredicate(predicate.kind, predicate.parameterName, predicate.parameterIndex, instantiateType(predicate.type, mapper));
18267+
return createTypePredicate(predicate.kind, predicate.parameterName, predicate.parameterIndex, instantiateType(predicate.type, mapper), predicate.oneSided);
1826318268
}
1826418269

1826518270
function instantiateSignature(signature: Signature, mapper: TypeMapper, eraseTypeParameters?: boolean): Signature {
@@ -19588,7 +19593,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
1958819593
}
1958919594
}
1959019595

19591-
const related = source.type === target.type ? Ternary.True :
19596+
const related = !source.oneSided && target.oneSided ? Ternary.False :
19597+
source.type === target.type ? Ternary.True :
1959219598
source.type && target.type ? compareTypes(source.type, target.type, reportErrors) :
1959319599
Ternary.False;
1959419600
if (related === Ternary.False && reportErrors) {
@@ -22630,6 +22636,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2263022636

2263122637
function compareTypePredicatesIdentical(source: TypePredicate | undefined, target: TypePredicate | undefined, compareTypes: (s: Type, t: Type) => Ternary): Ternary {
2263222638
return !(source && target && typePredicateKindsMatch(source, target)) ? Ternary.False :
22639+
source.oneSided !== target.oneSided ? Ternary.False :
2263322640
source.type === target.type ? Ternary.True :
2263422641
source.type && target.type ? compareTypes(source.type, target.type) :
2263522642
Ternary.False;
@@ -26890,15 +26897,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2689026897
return getNarrowedType(type, targetType, assumeTrue, /*checkDerived*/ true);
2689126898
}
2689226899

26893-
function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) {
26894-
const key = type.flags & TypeFlags.Union ? `N${getTypeId(type)},${getTypeId(candidate)},${(assumeTrue ? 1 : 0) | (checkDerived ? 2 : 0)}` : undefined;
26895-
return getCachedType(key) ?? setCachedType(key, getNarrowedTypeWorker(type, candidate, assumeTrue, checkDerived));
26900+
function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean, oneSided?: boolean) {
26901+
const key = type.flags & TypeFlags.Union ? `N${getTypeId(type)},${getTypeId(candidate)},${(assumeTrue ? 1 : 0) | (checkDerived ? 2 : 0) | (oneSided ? 4 : 0)}` : undefined;
26902+
return getCachedType(key) ?? setCachedType(key, getNarrowedTypeWorker(type, candidate, assumeTrue, checkDerived, oneSided));
2689626903
}
2689726904

26898-
function getNarrowedTypeWorker(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) {
26905+
function getNarrowedTypeWorker(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean, oneSided?: boolean) {
2689926906
const isRelated = checkDerived ? isTypeDerivedFrom : isTypeSubtypeOf;
2690026907
if (!assumeTrue) {
26901-
return filterType(type, t => !isRelated(t, candidate));
26908+
return oneSided ? type : filterType(type, t => !isRelated(t, candidate));
2690226909
}
2690326910
if (type.flags & TypeFlags.AnyOrUnknown) {
2690426911
return candidate;
@@ -26959,15 +26966,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2695926966
const predicateArgument = getTypePredicateArgument(predicate, callExpression);
2696026967
if (predicateArgument) {
2696126968
if (isMatchingReference(reference, predicateArgument)) {
26962-
return getNarrowedType(type, predicate.type, assumeTrue, /*checkDerived*/ false);
26969+
return getNarrowedType(type, predicate.type, assumeTrue, /*checkDerived*/ false, predicate.oneSided);
2696326970
}
2696426971
if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) &&
2696526972
!(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) {
2696626973
type = getAdjustedTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
2696726974
}
2696826975
const access = getDiscriminantPropertyAccess(predicateArgument, type);
2696926976
if (access) {
26970-
return narrowTypeByDiscriminant(type, access, t => getNarrowedType(t, predicate.type!, assumeTrue, /*checkDerived*/ false));
26977+
return narrowTypeByDiscriminant(type, access, t => getNarrowedType(t, predicate.type!, assumeTrue, /*checkDerived*/ false, predicate.oneSided));
2697126978
}
2697226979
}
2697326980
}

src/compiler/emitter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2668,6 +2668,10 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
26682668
writeSpace();
26692669
writeKeyword("is");
26702670
writeSpace();
2671+
if (node.subtypeOfModifier) {
2672+
emit(node.subtypeOfModifier);
2673+
writeSpace();
2674+
}
26712675
emit(node.type);
26722676
}
26732677
}

src/compiler/factory/nodeFactory.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ import {
411411
Statement,
412412
StringLiteral,
413413
stringToToken,
414+
SubtypeOfKeyword,
414415
SuperExpression,
415416
SwitchStatement,
416417
SyntaxKind,
@@ -2135,21 +2136,23 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
21352136
}
21362137

21372138
// @api
2138-
function createTypePredicateNode(assertsModifier: AssertsKeyword | undefined, parameterName: Identifier | ThisTypeNode | string, type: TypeNode | undefined) {
2139+
function createTypePredicateNode(assertsModifier: AssertsKeyword | undefined, parameterName: Identifier | ThisTypeNode | string, type: TypeNode | undefined, subtypeOfModifier?: SubtypeOfKeyword) {
21392140
const node = createBaseNode<TypePredicateNode>(SyntaxKind.TypePredicate);
21402141
node.assertsModifier = assertsModifier;
21412142
node.parameterName = asName(parameterName);
21422143
node.type = type;
21432144
node.transformFlags = TransformFlags.ContainsTypeScript;
2145+
node.subtypeOfModifier = subtypeOfModifier;
21442146
return node;
21452147
}
21462148

21472149
// @api
2148-
function updateTypePredicateNode(node: TypePredicateNode, assertsModifier: AssertsKeyword | undefined, parameterName: Identifier | ThisTypeNode, type: TypeNode | undefined) {
2150+
function updateTypePredicateNode(node: TypePredicateNode, assertsModifier: AssertsKeyword | undefined, parameterName: Identifier | ThisTypeNode, type: TypeNode | undefined, subtypeOfModifier?: SubtypeOfKeyword) {
21492151
return node.assertsModifier !== assertsModifier
21502152
|| node.parameterName !== parameterName
21512153
|| node.type !== type
2152-
? update(createTypePredicateNode(assertsModifier, parameterName, type), node)
2154+
|| node.subtypeOfModifier !== subtypeOfModifier
2155+
? update(createTypePredicateNode(assertsModifier, parameterName, type, subtypeOfModifier), node)
21532156
: node;
21542157
}
21552158

src/compiler/factory/nodeTests.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ import {
187187
SpreadElement,
188188
StaticKeyword,
189189
StringLiteral,
190+
SubtypeOfKeyword,
190191
SuperExpression,
191192
SwitchStatement,
192193
SyntaxKind,
@@ -387,6 +388,11 @@ export function isCaseKeyword(node: Node): node is CaseKeyword {
387388
return node.kind === SyntaxKind.CaseKeyword;
388389
}
389390

391+
/** @internal */
392+
export function isSubtypeOfKeyword(node: Node): node is SubtypeOfKeyword {
393+
return node.kind === SyntaxKind.SubtypeOfKeyword;
394+
}
395+
390396
// Names
391397

392398
export function isQualifiedName(node: Node): node is QualifiedName {

src/compiler/parser.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,7 @@ const forEachChildTable: ForEachChildTable = {
660660
[SyntaxKind.TypePredicate]: function forEachChildInTypePredicate<T>(node: TypePredicateNode, cbNode: (node: Node) => T | undefined, _cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
661661
return visitNode(cbNode, node.assertsModifier) ||
662662
visitNode(cbNode, node.parameterName) ||
663+
visitNode(cbNode, node.subtypeOfModifier) ||
663664
visitNode(cbNode, node.type);
664665
},
665666
[SyntaxKind.TypeQuery]: function forEachChildInTypeQuery<T>(node: TypeQueryNode, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
@@ -3689,7 +3690,8 @@ namespace Parser {
36893690

36903691
function parseThisTypePredicate(lhs: ThisTypeNode): TypePredicateNode {
36913692
nextToken();
3692-
return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, lhs, parseType()), lhs.pos);
3693+
const subtypeOfModifier = parseOptionalToken(SyntaxKind.SubtypeOfKeyword);
3694+
return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, lhs, parseType(), subtypeOfModifier), lhs.pos);
36933695
}
36943696

36953697
function parseThisTypeNode(): ThisTypeNode {
@@ -4772,12 +4774,12 @@ namespace Parser {
47724774
function parseTypeOrTypePredicate(): TypeNode {
47734775
const pos = getNodePos();
47744776
const typePredicateVariable = isIdentifier() && tryParse(parseTypePredicatePrefix);
4775-
const type = parseType();
47764777
if (typePredicateVariable) {
4777-
return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, typePredicateVariable, type), pos);
4778+
const subtypeOfModifier = parseOptionalToken(SyntaxKind.SubtypeOfKeyword);
4779+
return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, typePredicateVariable, parseType(), subtypeOfModifier), pos);
47784780
}
47794781
else {
4780-
return type;
4782+
return parseType();
47814783
}
47824784
}
47834785

@@ -4793,8 +4795,13 @@ namespace Parser {
47934795
const pos = getNodePos();
47944796
const assertsModifier = parseExpectedToken(SyntaxKind.AssertsKeyword);
47954797
const parameterName = token() === SyntaxKind.ThisKeyword ? parseThisTypeNode() : parseIdentifier();
4796-
const type = parseOptional(SyntaxKind.IsKeyword) ? parseType() : undefined;
4797-
return finishNode(factory.createTypePredicateNode(assertsModifier, parameterName, type), pos);
4798+
let subtypeOfModifier: Token<SyntaxKind.SubtypeOfKeyword> | undefined;
4799+
let type: TypeNode | undefined;
4800+
if (parseOptional(SyntaxKind.IsKeyword)) {
4801+
subtypeOfModifier = parseOptionalToken(SyntaxKind.SubtypeOfKeyword);
4802+
type = parseType();
4803+
}
4804+
return finishNode(factory.createTypePredicateNode(assertsModifier, parameterName, type, subtypeOfModifier), pos);
47984805
}
47994806

48004807
function parseType(): TypeNode {

src/compiler/scanner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export const textToKeywordObj: MapLike<KeywordSyntaxKind> = {
169169
set: SyntaxKind.SetKeyword,
170170
static: SyntaxKind.StaticKeyword,
171171
string: SyntaxKind.StringKeyword,
172+
subtypeof: SyntaxKind.SubtypeOfKeyword,
172173
super: SyntaxKind.SuperKeyword,
173174
switch: SyntaxKind.SwitchKeyword,
174175
symbol: SyntaxKind.SymbolKeyword,

0 commit comments

Comments
 (0)