Skip to content

Commit

Permalink
Introduce structural tag T types
Browse files Browse the repository at this point in the history
  • Loading branch information
weswigham committed Sep 6, 2019
1 parent fb453f8 commit aa08d39
Show file tree
Hide file tree
Showing 27 changed files with 2,295 additions and 9 deletions.
67 changes: 63 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ namespace ts {
const literalTypes = createMap<LiteralType>();
const indexedAccessTypes = createMap<IndexedAccessType>();
const substitutionTypes = createMap<SubstitutionType>();
const structuralTags = createMap<StructuralTagType>();
const evolvingArrayTypes: EvolvingArrayType[] = [];
const undefinedProperties = createMap<Symbol>() as UnderscoreEscapedMap<Symbol>;

Expand Down Expand Up @@ -3799,6 +3800,10 @@ namespace ts {
if (type.flags & TypeFlags.Substitution) {
return typeToTypeNodeHelper((<SubstitutionType>type).typeVariable, context);
}
if (type.flags & TypeFlags.StructuralTag) {
context.approximateLength += 4;
return createTypeOperatorNode(SyntaxKind.TagKeyword, typeToTypeNodeHelper((type as StructuralTagType).type, context));
}

return Debug.fail("Should be unreachable.");

Expand Down Expand Up @@ -8103,6 +8108,9 @@ namespace ts {
if (t.flags & TypeFlags.Substitution) {
return getBaseConstraint((<SubstitutionType>t).substitute);
}
if (t.flags & TypeFlags.StructuralTag) {
return unknownType;
}
return t;
}
}
Expand Down Expand Up @@ -9924,10 +9932,10 @@ namespace ts {
return links.resolvedType;
}

function addTypeToIntersection(typeSet: Map<Type>, includes: TypeFlags, type: Type) {
function addTypeToIntersection(typeSet: Map<Type>, includes: TypeFlags, type: Type, tagSet: Map<StructuralTagType>) {
const flags = type.flags;
if (flags & TypeFlags.Intersection) {
return addTypesToIntersection(typeSet, includes, (<IntersectionType>type).types);
return addTypesToIntersection(typeSet, includes, (<IntersectionType>type).types, tagSet);
}
if (isEmptyAnonymousObjectType(type)) {
if (!(includes & TypeFlags.IncludesEmptyObject)) {
Expand All @@ -9939,6 +9947,9 @@ namespace ts {
if (flags & TypeFlags.AnyOrUnknown) {
if (type === wildcardType) includes |= TypeFlags.IncludesWildcard;
}
else if (flags & TypeFlags.StructuralTag) {
tagSet.set(type.id.toString(), type as StructuralTagType);
}
else if ((strictNullChecks || !(flags & TypeFlags.Nullable)) && !typeSet.has(type.id.toString())) {
if (type.flags & TypeFlags.Unit && includes & TypeFlags.Unit) {
// We have seen two distinct unit types which means we should reduce to an
Expand All @@ -9954,9 +9965,23 @@ namespace ts {

// Add the given types to the given type set. Order is preserved, freshness is removed from literal
// types, duplicates are removed, and nested types of the given kind are flattened into the set.
function addTypesToIntersection(typeSet: Map<Type>, includes: TypeFlags, types: ReadonlyArray<Type>) {
function addTypesToIntersection(typeSet: Map<Type>, includes: TypeFlags, types: ReadonlyArray<Type>, tagSet?: Map<StructuralTagType> | undefined) {
const isTopLevel = !tagSet;
tagSet = tagSet || createMap();
for (const type of types) {
includes = addTypeToIntersection(typeSet, includes, getRegularTypeOfLiteralType(type));
includes = addTypeToIntersection(typeSet, includes, getRegularTypeOfLiteralType(type), tagSet);
}
if (isTopLevel && tagSet.size) {
let tag: StructuralTagType;
if (tagSet.size === 1) {
tag = tagSet.values().next().value;
}
else {
const tagTypes: Type[] = [];
tagSet.forEach(t => tagTypes.push(t.type));
tag = getStructuralTagForType(getIntersectionType(tagTypes));
}
typeSet.set(tag.id.toString(), tag);
}
return includes;
}
Expand Down Expand Up @@ -10258,6 +10283,11 @@ namespace ts {
case SyntaxKind.ReadonlyKeyword:
links.resolvedType = getTypeFromTypeNode(node.type);
break;
case SyntaxKind.TagKeyword:
const aliasSymbol = getAliasSymbolForTypeNode(node);
const aliasParams = getTypeArgumentsForAliasSymbol(aliasSymbol);
links.resolvedType = getStructuralTagForType(getTypeFromTypeNode(node.type), aliasSymbol, aliasParams);
break;
default:
throw Debug.assertNever(node.operator);
}
Expand All @@ -10272,6 +10302,19 @@ namespace ts {
return type;
}

function getStructuralTagForType(type: Type, aliasSymbol?: Symbol, aliasTypeArguments?: readonly Type[]) {
const tid = "" + getTypeId(type);
if (structuralTags.has(tid)) {
return structuralTags.get(tid)!;
}
const tag = createType(TypeFlags.StructuralTag) as StructuralTagType;
tag.type = type;
tag.aliasSymbol = aliasSymbol;
tag.aliasTypeArguments = aliasTypeArguments;
structuralTags.set(tid, tag);
return tag;
}

/**
* Returns if a type is or consists of a JSLiteral object type
* In addition to objects which are directly literals,
Expand Down Expand Up @@ -11706,6 +11749,10 @@ namespace ts {
return sub;
}
}
if (flags & TypeFlags.StructuralTag) {
const newType = instantiateType((type as StructuralTagType).type, mapper);
return newType !== type ? getStructuralTagForType(newType, type.aliasSymbol, instantiateTypes(type.aliasTypeArguments, mapper)) : type;
}
return type;
}

Expand Down Expand Up @@ -13422,6 +13469,9 @@ namespace ts {
if (flags & TypeFlags.Substitution) {
return isRelatedTo((<SubstitutionType>source).substitute, (<SubstitutionType>target).substitute, /*reportErrors*/ false);
}
if (flags & TypeFlags.StructuralTag) {
return isRelatedTo((<StructuralTagType>source).type, (<StructuralTagType>target).type, /*reportErrors*/ false);
}
return Ternary.False;
}

Expand Down Expand Up @@ -13524,6 +13574,12 @@ namespace ts {
}
}
}
else if (target.flags & TypeFlags.StructuralTag) {
if (source.flags & TypeFlags.StructuralTag) {
return isRelatedTo((source as StructuralTagType).type, (target as StructuralTagType).type, reportErrors);
}
return Ternary.False;
}

if (source.flags & TypeFlags.TypeVariable) {
if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) {
Expand Down Expand Up @@ -15727,6 +15783,9 @@ namespace ts {
inferFromTypes((<IndexType>source).type, (<IndexType>target).type);
contravariant = !contravariant;
}
else if (source.flags & TypeFlags.StructuralTag && target.flags & TypeFlags.StructuralTag) {
inferFromTypes((<StructuralTagType>source).type, (<StructuralTagType>target).type);
}
else if ((isLiteralType(source) || source.flags & TypeFlags.String) && target.flags & TypeFlags.Index) {
const empty = createEmptyObjectTypeFromStringLiteral(source);
contravariant = !contravariant;
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,8 +876,8 @@ namespace ts {
}

export function createTypeOperatorNode(type: TypeNode): TypeOperatorNode;
export function createTypeOperatorNode(operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword, type: TypeNode): TypeOperatorNode;
export function createTypeOperatorNode(operatorOrType: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword | TypeNode, type?: TypeNode) {
export function createTypeOperatorNode(operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword | SyntaxKind.TagKeyword, type: TypeNode): TypeOperatorNode;
export function createTypeOperatorNode(operatorOrType: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword | SyntaxKind.TagKeyword | TypeNode, type?: TypeNode) {
const node = createSynthesizedNode(SyntaxKind.TypeOperator) as TypeOperatorNode;
node.operator = typeof operatorOrType === "number" ? operatorOrType : SyntaxKind.KeyOfKeyword;
node.type = parenthesizeElementTypeMember(typeof operatorOrType === "number" ? type! : operatorOrType);
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3053,6 +3053,7 @@ namespace ts {
case SyntaxKind.ReadonlyKeyword:
case SyntaxKind.SymbolKeyword:
case SyntaxKind.UniqueKeyword:
case SyntaxKind.TagKeyword:
case SyntaxKind.VoidKeyword:
case SyntaxKind.UndefinedKeyword:
case SyntaxKind.NullKeyword:
Expand Down Expand Up @@ -3140,7 +3141,7 @@ namespace ts {
return finishNode(postfix);
}

function parseTypeOperator(operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword) {
function parseTypeOperator(operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword | SyntaxKind.TagKeyword) {
const node = <TypeOperatorNode>createNode(SyntaxKind.TypeOperator);
parseExpected(operator);
node.operator = operator;
Expand All @@ -3163,6 +3164,7 @@ namespace ts {
case SyntaxKind.KeyOfKeyword:
case SyntaxKind.UniqueKeyword:
case SyntaxKind.ReadonlyKeyword:
case SyntaxKind.TagKeyword:
return parseTypeOperator(operator);
case SyntaxKind.InferKeyword:
return parseInferType();
Expand Down
1 change: 1 addition & 0 deletions src/compiler/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ namespace ts {
async: SyntaxKind.AsyncKeyword,
await: SyntaxKind.AwaitKeyword,
of: SyntaxKind.OfKeyword,
tag: SyntaxKind.TagKeyword,
};

const textToKeyword = createMapFromTemplate(textToKeywordObj);
Expand Down
12 changes: 10 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ namespace ts {
| SyntaxKind.YieldKeyword
| SyntaxKind.AsyncKeyword
| SyntaxKind.AwaitKeyword
| SyntaxKind.TagKeyword
| SyntaxKind.OfKeyword;

export type JsxTokenSyntaxKind =
Expand Down Expand Up @@ -277,6 +278,7 @@ namespace ts {
FromKeyword,
GlobalKeyword,
BigIntKeyword,
TagKeyword,
OfKeyword, // LastKeyword and LastToken and LastContextualKeyword

// Parse tree nodes
Expand Down Expand Up @@ -1246,7 +1248,7 @@ namespace ts {

export interface TypeOperatorNode extends TypeNode {
kind: SyntaxKind.TypeOperator;
operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword;
operator: SyntaxKind.KeyOfKeyword | SyntaxKind.UniqueKeyword | SyntaxKind.ReadonlyKeyword | SyntaxKind.TagKeyword;
type: TypeNode;
}

Expand Down Expand Up @@ -3984,6 +3986,7 @@ namespace ts {
Conditional = 1 << 24, // T extends U ? X : Y
Substitution = 1 << 25, // Type parameter substitution
NonPrimitive = 1 << 26, // intrinsic object type
StructuralTag = 1 << 27, // tag T

/* @internal */
AnyOrUnknown = Any | Unknown,
Expand Down Expand Up @@ -4013,7 +4016,7 @@ namespace ts {
UnionOrIntersection = Union | Intersection,
StructuredType = Object | Union | Intersection,
TypeVariable = TypeParameter | IndexedAccess,
InstantiableNonPrimitive = TypeVariable | Conditional | Substitution,
InstantiableNonPrimitive = TypeVariable | Conditional | Substitution | StructuralTag,
InstantiablePrimitive = Index,
Instantiable = InstantiableNonPrimitive | InstantiablePrimitive,
StructuredOrInstantiable = StructuredType | Instantiable,
Expand Down Expand Up @@ -4431,6 +4434,11 @@ namespace ts {
substitute: Type; // Type to substitute for type parameter
}

// Structual tag type, or a `tag T` (TypeFlags.StructuralTag)
export interface StructuralTagType extends InstantiableType {
type: Type;
}

/* @internal */
export const enum JsxReferenceKind {
Component,
Expand Down
139 changes: 139 additions & 0 deletions tests/baselines/reference/structuralTagTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//// [structuralTagTypes.ts]
export type Downcased = string & tag { downcased: void; };
export type Analyzed<T> = T & tag { analyzed: void; };
export type Paired = {
x: number & tag {x: number;};
y: number & tag {y: number;};
};

export function downcase(x: string): Downcased {
return x.toLocaleLowerCase() as Downcased;
}

export function downcaseLit<T extends string>(x: T): T & Downcased {
return x.toLocaleLowerCase() as T & Downcased;
}

export function isDowncase(x: string): x is Downcased {
return null as any;
}

export function analyze<T>(x: T): Analyzed<T> {
return x as Analyzed<T>;
}

export function isAnalyzed<T>(x: T): x is Analyzed<T> {
return Math.random() > 0.33 ? false : true;
}

export function isPaired(x: {x: number, y: number}): x is Paired {
return true;
}

export function makePair(x: number, y: number): Paired {
return {x, y} as Paired;
}

const a = "ok";
export const b = downcase(a);
export const d = downcaseLit(b);

if (isDowncase(d)) {
d;
}

const e = {data: { value: "str" }};
export const f = analyze(e);
if (isAnalyzed(e)) {
e;
}

export const g = makePair(0, 0);
const h = {x: 0, y: 0};
if (isPaired(h)) {
h;
}


//// [structuralTagTypes.js]
"use strict";
exports.__esModule = true;
function downcase(x) {
return x.toLocaleLowerCase();
}
exports.downcase = downcase;
function downcaseLit(x) {
return x.toLocaleLowerCase();
}
exports.downcaseLit = downcaseLit;
function isDowncase(x) {
return null;
}
exports.isDowncase = isDowncase;
function analyze(x) {
return x;
}
exports.analyze = analyze;
function isAnalyzed(x) {
return Math.random() > 0.33 ? false : true;
}
exports.isAnalyzed = isAnalyzed;
function isPaired(x) {
return true;
}
exports.isPaired = isPaired;
function makePair(x, y) {
return { x: x, y: y };
}
exports.makePair = makePair;
var a = "ok";
exports.b = downcase(a);
exports.d = downcaseLit(exports.b);
if (isDowncase(exports.d)) {
exports.d;
}
var e = { data: { value: "str" } };
exports.f = analyze(e);
if (isAnalyzed(e)) {
e;
}
exports.g = makePair(0, 0);
var h = { x: 0, y: 0 };
if (isPaired(h)) {
h;
}


//// [structuralTagTypes.d.ts]
export declare type Downcased = string & tag {
downcased: void;
};
export declare type Analyzed<T> = T & tag {
analyzed: void;
};
export declare type Paired = {
x: number & tag {
x: number;
};
y: number & tag {
y: number;
};
};
export declare function downcase(x: string): Downcased;
export declare function downcaseLit<T extends string>(x: T): T & Downcased;
export declare function isDowncase(x: string): x is Downcased;
export declare function analyze<T>(x: T): Analyzed<T>;
export declare function isAnalyzed<T>(x: T): x is Analyzed<T>;
export declare function isPaired(x: {
x: number;
y: number;
}): x is Paired;
export declare function makePair(x: number, y: number): Paired;
export declare const b: Downcased;
export declare const d: Downcased;
export declare const f: Analyzed<{
data: {
value: string;
};
}>;
export declare const g: Paired;
Loading

0 comments on commit aa08d39

Please sign in to comment.