Skip to content

Commit

Permalink
feat(15048): One-sided Type Predicates
Browse files Browse the repository at this point in the history
  • Loading branch information
graphemecluster committed Jan 15, 2023
1 parent 8586c99 commit 9dfa74d
Show file tree
Hide file tree
Showing 15 changed files with 972 additions and 475 deletions.
41 changes: 24 additions & 17 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1940,7 +1940,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const markerSubTypeForCheck = createTypeParameter();
markerSubTypeForCheck.constraint = markerSuperTypeForCheck;

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

const anySignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None);
const unknownSignature = createSignature(undefined, undefined, undefined, emptyArray, errorType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None);
Expand Down Expand Up @@ -7156,7 +7156,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
setEmitFlags(factory.createIdentifier(typePredicate.parameterName), EmitFlags.NoAsciiEscaping) :
factory.createThisTypeNode();
const typeNode = typePredicate.type && typeToTypeNodeHelper(typePredicate.type, context);
returnTypeNode = factory.createTypePredicateNode(assertsModifier, parameterName, typeNode);
const subtypeOfModifier = typePredicate.oneSided ? factory.createToken(SyntaxKind.SubtypeOfKeyword) : undefined;
returnTypeNode = factory.createTypePredicateNode(assertsModifier, parameterName, typeNode, subtypeOfModifier);
}
else {
const returnType = getReturnTypeOfSignature(signature);
Expand Down Expand Up @@ -9528,7 +9529,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const predicate = factory.createTypePredicateNode(
typePredicate.kind === TypePredicateKind.AssertsThis || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? factory.createToken(SyntaxKind.AssertsKeyword) : undefined,
typePredicate.kind === TypePredicateKind.Identifier || typePredicate.kind === TypePredicateKind.AssertsIdentifier ? factory.createIdentifier(typePredicate.parameterName) : factory.createThisTypeNode(),
typePredicate.type && nodeBuilder.typeToTypeNode(typePredicate.type, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName)! // TODO: GH#18217
typePredicate.type && nodeBuilder.typeToTypeNode(typePredicate.type, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName)!, // TODO: GH#18217
typePredicate.oneSided ? factory.createToken(SyntaxKind.SubtypeOfKeyword) : undefined
);
const printer = createPrinter({ removeComments: true });
const sourceFile = enclosingDeclaration && getSourceFileOfNode(enclosingDeclaration);
Expand Down Expand Up @@ -14083,8 +14085,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return isPropertyDeclaration(node) && !hasAccessorModifier(node) && node.questionToken;
}

function createTypePredicate(kind: TypePredicateKind, parameterName: string | undefined, parameterIndex: number | undefined, type: Type | undefined): TypePredicate {
return { kind, parameterName, parameterIndex, type } as TypePredicate;
function createTypePredicate(kind: TypePredicateKind, parameterName: string | undefined, parameterIndex: number | undefined, type: Type | undefined, oneSided: boolean): TypePredicate {
return { kind, parameterName, parameterIndex, type, oneSided } as TypePredicate;
}

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

function getUnionOrIntersectionType(types: Type[], kind: TypeFlags | undefined, unionReduction?: UnionReduction) {
Expand Down Expand Up @@ -16075,6 +16077,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function getUnionOrIntersectionTypePredicate(signatures: readonly Signature[], kind: TypeFlags | undefined): TypePredicate | undefined {
let first: TypePredicate | undefined;
const types: Type[] = [];
let oneSided!: boolean;
for (const sig of signatures) {
const pred = getTypePredicateOfSignature(sig);
if (!pred || pred.kind === TypePredicateKind.AssertsThis || pred.kind === TypePredicateKind.AssertsIdentifier) {
Expand All @@ -16091,9 +16094,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// No common type predicate.
return undefined;
}
oneSided = kind === TypeFlags.Intersection ? oneSided || pred.oneSided : oneSided && pred.oneSided;
}
else {
first = pred;
oneSided = pred.oneSided;
}
types.push(pred.type);
}
Expand All @@ -16102,7 +16107,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return undefined;
}
const compositeType = getUnionOrIntersectionType(types, kind);
return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, compositeType);
return createTypePredicate(first.kind, first.parameterName, first.parameterIndex, compositeType, oneSided);
}

function typePredicateKindsMatch(a: TypePredicate, b: TypePredicate): boolean {
Expand Down Expand Up @@ -18259,7 +18264,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function instantiateTypePredicate(predicate: TypePredicate, mapper: TypeMapper): TypePredicate {
return createTypePredicate(predicate.kind, predicate.parameterName, predicate.parameterIndex, instantiateType(predicate.type, mapper));
return createTypePredicate(predicate.kind, predicate.parameterName, predicate.parameterIndex, instantiateType(predicate.type, mapper), predicate.oneSided);
}

function instantiateSignature(signature: Signature, mapper: TypeMapper, eraseTypeParameters?: boolean): Signature {
Expand Down Expand Up @@ -19588,7 +19593,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}

const related = source.type === target.type ? Ternary.True :
const related = !source.oneSided && target.oneSided ? Ternary.False :
source.type === target.type ? Ternary.True :
source.type && target.type ? compareTypes(source.type, target.type, reportErrors) :
Ternary.False;
if (related === Ternary.False && reportErrors) {
Expand Down Expand Up @@ -22630,6 +22636,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

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

function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) {
const key = type.flags & TypeFlags.Union ? `N${getTypeId(type)},${getTypeId(candidate)},${(assumeTrue ? 1 : 0) | (checkDerived ? 2 : 0)}` : undefined;
return getCachedType(key) ?? setCachedType(key, getNarrowedTypeWorker(type, candidate, assumeTrue, checkDerived));
function getNarrowedType(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean, oneSided?: boolean) {
const key = type.flags & TypeFlags.Union ? `N${getTypeId(type)},${getTypeId(candidate)},${(assumeTrue ? 1 : 0) | (checkDerived ? 2 : 0) | (oneSided ? 4 : 0)}` : undefined;
return getCachedType(key) ?? setCachedType(key, getNarrowedTypeWorker(type, candidate, assumeTrue, checkDerived, oneSided));
}

function getNarrowedTypeWorker(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean) {
function getNarrowedTypeWorker(type: Type, candidate: Type, assumeTrue: boolean, checkDerived: boolean, oneSided?: boolean) {
const isRelated = checkDerived ? isTypeDerivedFrom : isTypeSubtypeOf;
if (!assumeTrue) {
return filterType(type, t => !isRelated(t, candidate));
return oneSided ? type : filterType(type, t => !isRelated(t, candidate));
}
if (type.flags & TypeFlags.AnyOrUnknown) {
return candidate;
Expand Down Expand Up @@ -26959,15 +26966,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const predicateArgument = getTypePredicateArgument(predicate, callExpression);
if (predicateArgument) {
if (isMatchingReference(reference, predicateArgument)) {
return getNarrowedType(type, predicate.type, assumeTrue, /*checkDerived*/ false);
return getNarrowedType(type, predicate.type, assumeTrue, /*checkDerived*/ false, predicate.oneSided);
}
if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) &&
!(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) {
type = getAdjustedTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
}
const access = getDiscriminantPropertyAccess(predicateArgument, type);
if (access) {
return narrowTypeByDiscriminant(type, access, t => getNarrowedType(t, predicate.type!, assumeTrue, /*checkDerived*/ false));
return narrowTypeByDiscriminant(type, access, t => getNarrowedType(t, predicate.type!, assumeTrue, /*checkDerived*/ false, predicate.oneSided));
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2668,6 +2668,10 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
writeSpace();
writeKeyword("is");
writeSpace();
if (node.subtypeOfModifier) {
emit(node.subtypeOfModifier);
writeSpace();
}
emit(node.type);
}
}
Expand Down
9 changes: 6 additions & 3 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ import {
Statement,
StringLiteral,
stringToToken,
SubtypeOfKeyword,
SuperExpression,
SwitchStatement,
SyntaxKind,
Expand Down Expand Up @@ -2135,21 +2136,23 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function createTypePredicateNode(assertsModifier: AssertsKeyword | undefined, parameterName: Identifier | ThisTypeNode | string, type: TypeNode | undefined) {
function createTypePredicateNode(assertsModifier: AssertsKeyword | undefined, parameterName: Identifier | ThisTypeNode | string, type: TypeNode | undefined, subtypeOfModifier?: SubtypeOfKeyword) {
const node = createBaseNode<TypePredicateNode>(SyntaxKind.TypePredicate);
node.assertsModifier = assertsModifier;
node.parameterName = asName(parameterName);
node.type = type;
node.transformFlags = TransformFlags.ContainsTypeScript;
node.subtypeOfModifier = subtypeOfModifier;
return node;
}

// @api
function updateTypePredicateNode(node: TypePredicateNode, assertsModifier: AssertsKeyword | undefined, parameterName: Identifier | ThisTypeNode, type: TypeNode | undefined) {
function updateTypePredicateNode(node: TypePredicateNode, assertsModifier: AssertsKeyword | undefined, parameterName: Identifier | ThisTypeNode, type: TypeNode | undefined, subtypeOfModifier?: SubtypeOfKeyword) {
return node.assertsModifier !== assertsModifier
|| node.parameterName !== parameterName
|| node.type !== type
? update(createTypePredicateNode(assertsModifier, parameterName, type), node)
|| node.subtypeOfModifier !== subtypeOfModifier
? update(createTypePredicateNode(assertsModifier, parameterName, type, subtypeOfModifier), node)
: node;
}

Expand Down
6 changes: 6 additions & 0 deletions src/compiler/factory/nodeTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ import {
SpreadElement,
StaticKeyword,
StringLiteral,
SubtypeOfKeyword,
SuperExpression,
SwitchStatement,
SyntaxKind,
Expand Down Expand Up @@ -387,6 +388,11 @@ export function isCaseKeyword(node: Node): node is CaseKeyword {
return node.kind === SyntaxKind.CaseKeyword;
}

/** @internal */
export function isSubtypeOfKeyword(node: Node): node is SubtypeOfKeyword {
return node.kind === SyntaxKind.SubtypeOfKeyword;
}

// Names

export function isQualifiedName(node: Node): node is QualifiedName {
Expand Down
19 changes: 13 additions & 6 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ const forEachChildTable: ForEachChildTable = {
[SyntaxKind.TypePredicate]: function forEachChildInTypePredicate<T>(node: TypePredicateNode, cbNode: (node: Node) => T | undefined, _cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
return visitNode(cbNode, node.assertsModifier) ||
visitNode(cbNode, node.parameterName) ||
visitNode(cbNode, node.subtypeOfModifier) ||
visitNode(cbNode, node.type);
},
[SyntaxKind.TypeQuery]: function forEachChildInTypeQuery<T>(node: TypeQueryNode, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
Expand Down Expand Up @@ -3689,7 +3690,8 @@ namespace Parser {

function parseThisTypePredicate(lhs: ThisTypeNode): TypePredicateNode {
nextToken();
return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, lhs, parseType()), lhs.pos);
const subtypeOfModifier = parseOptionalToken(SyntaxKind.SubtypeOfKeyword);
return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, lhs, parseType(), subtypeOfModifier), lhs.pos);
}

function parseThisTypeNode(): ThisTypeNode {
Expand Down Expand Up @@ -4772,12 +4774,12 @@ namespace Parser {
function parseTypeOrTypePredicate(): TypeNode {
const pos = getNodePos();
const typePredicateVariable = isIdentifier() && tryParse(parseTypePredicatePrefix);
const type = parseType();
if (typePredicateVariable) {
return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, typePredicateVariable, type), pos);
const subtypeOfModifier = parseOptionalToken(SyntaxKind.SubtypeOfKeyword);
return finishNode(factory.createTypePredicateNode(/*assertsModifier*/ undefined, typePredicateVariable, parseType(), subtypeOfModifier), pos);
}
else {
return type;
return parseType();
}
}

Expand All @@ -4793,8 +4795,13 @@ namespace Parser {
const pos = getNodePos();
const assertsModifier = parseExpectedToken(SyntaxKind.AssertsKeyword);
const parameterName = token() === SyntaxKind.ThisKeyword ? parseThisTypeNode() : parseIdentifier();
const type = parseOptional(SyntaxKind.IsKeyword) ? parseType() : undefined;
return finishNode(factory.createTypePredicateNode(assertsModifier, parameterName, type), pos);
let subtypeOfModifier: Token<SyntaxKind.SubtypeOfKeyword> | undefined;
let type: TypeNode | undefined;
if (parseOptional(SyntaxKind.IsKeyword)) {
subtypeOfModifier = parseOptionalToken(SyntaxKind.SubtypeOfKeyword);
type = parseType();
}
return finishNode(factory.createTypePredicateNode(assertsModifier, parameterName, type, subtypeOfModifier), pos);
}

function parseType(): TypeNode {
Expand Down
1 change: 1 addition & 0 deletions src/compiler/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export const textToKeywordObj: MapLike<KeywordSyntaxKind> = {
set: SyntaxKind.SetKeyword,
static: SyntaxKind.StaticKeyword,
string: SyntaxKind.StringKeyword,
subtypeof: SyntaxKind.SubtypeOfKeyword,
super: SyntaxKind.SuperKeyword,
switch: SyntaxKind.SwitchKeyword,
symbol: SyntaxKind.SymbolKeyword,
Expand Down
Loading

0 comments on commit 9dfa74d

Please sign in to comment.