Skip to content

Commit 13968b0

Browse files
committed
Nominal type brands
1 parent 4bddf55 commit 13968b0

31 files changed

+2343
-62
lines changed

src/compiler/binder.ts

+9
Original file line numberDiff line numberDiff line change
@@ -2216,6 +2216,11 @@ namespace ts {
22162216
break; // Binding the children will handle everything
22172217
case SyntaxKind.TypeParameter:
22182218
return bindTypeParameter(node as TypeParameterDeclaration);
2219+
case SyntaxKind.TypeOperator:
2220+
if ((node as TypeOperatorNode).operator === SyntaxKind.UniqueKeyword) {
2221+
return bindUniqueKeyword(node as TypeOperatorNode);
2222+
}
2223+
break;
22192224
case SyntaxKind.Parameter:
22202225
return bindParameter(<ParameterDeclaration>node);
22212226
case SyntaxKind.VariableDeclaration:
@@ -2929,6 +2934,10 @@ namespace ts {
29292934
}
29302935
}
29312936

2937+
function bindUniqueKeyword(node: TypeOperatorNode) {
2938+
addDeclarationToSymbol(createSymbol(SymbolFlags.NominalBrand, InternalSymbolName.NominalBrand), node, SymbolFlags.NominalBrand);
2939+
}
2940+
29322941
// reachability checks
29332942

29342943
function shouldReportErrorOnModuleDeclaration(node: ModuleDeclaration): boolean {

src/compiler/checker.ts

+102-34
Original file line numberDiff line numberDiff line change
@@ -3752,7 +3752,28 @@ namespace ts {
37523752
return symbolToTypeNode(type.aliasSymbol, context, SymbolFlags.Type, typeArgumentNodes);
37533753
}
37543754
if (type.flags & (TypeFlags.Union | TypeFlags.Intersection)) {
3755-
const types = type.flags & TypeFlags.Union ? formatUnionTypes((<UnionType>type).types) : (<IntersectionType>type).types;
3755+
const types = type.flags & TypeFlags.Union ? formatUnionTypes((<UnionType>type).types) : filter((<IntersectionType>type).types, t => !(context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope) || !(t.flags & TypeFlags.NominalBrand && !t.aliasSymbol));
3756+
if (context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope && type.flags & TypeFlags.Intersection && length(types) !== length((<IntersectionType>type).types)) {
3757+
// If an intersection had brands and no alias, add a `unique` to the front to indicate this
3758+
// the brands aren't _precisely_ recoverable by this printback, but it's a good indicator in the LS that there's an unutterable nominal tag on the type
3759+
// Ideally, we could hyperlink the `unique` we manufacture here to each of the brand locations it refers to
3760+
context.approximateLength += 7;
3761+
if (length(types) === 0) {
3762+
context.approximateLength += 7;
3763+
return createTypeOperatorNode(SyntaxKind.UniqueKeyword, createKeywordTypeNode(SyntaxKind.UnknownKeyword));
3764+
}
3765+
if (length(types) === 1) {
3766+
return createTypeOperatorNode(SyntaxKind.UniqueKeyword, typeToTypeNodeHelper(types[0], context));
3767+
}
3768+
const nodes = mapToTypeNodes(types, context, /*isBareList*/ true);
3769+
if (!length(nodes)) {
3770+
if (!context.encounteredError && !(context.flags & NodeBuilderFlags.AllowEmptyUnionOrIntersection)) {
3771+
context.encounteredError = true;
3772+
}
3773+
return undefined!; // TODO: GH#18217
3774+
}
3775+
return createTypeOperatorNode(SyntaxKind.UniqueKeyword, createUnionOrIntersectionTypeNode(SyntaxKind.IntersectionType, nodes!));
3776+
}
37563777
if (length(types) === 1) {
37573778
return typeToTypeNodeHelper(types[0], context);
37583779
}
@@ -3799,6 +3820,17 @@ namespace ts {
37993820
if (type.flags & TypeFlags.Substitution) {
38003821
return typeToTypeNodeHelper((<SubstitutionType>type).typeVariable, context);
38013822
}
3823+
if (type.flags & TypeFlags.NominalBrand) {
3824+
if (!context.encounteredError && !(context.flags & NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope) && !(context.flags & NodeBuilderFlags.AllowEmptyUnionOrIntersection)) {
3825+
context.encounteredError = true;
3826+
if (context.tracker && context.tracker.trackSymbol) {
3827+
// TODO: issue a custom error message about brand name not being directly accessible
3828+
context.tracker.trackSymbol(type.symbol, context.enclosingDeclaration, SymbolFlags.Type);
3829+
}
3830+
}
3831+
context.approximateLength += 14;
3832+
return createTypeOperatorNode(SyntaxKind.UniqueKeyword, createKeywordTypeNode(SyntaxKind.UnknownKeyword));
3833+
}
38023834

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

@@ -10206,7 +10238,7 @@ namespace ts {
1020610238
maybeTypeOfKind(type, TypeFlags.InstantiableNonPrimitive) ? getIndexTypeForGenericType(<InstantiableType | UnionOrIntersectionType>type, stringsOnly) :
1020710239
getObjectFlags(type) & ObjectFlags.Mapped ? filterType(getConstraintTypeFromMappedType(<MappedType>type), t => !(noIndexSignatures && t.flags & (TypeFlags.Any | TypeFlags.String))) :
1020810240
type === wildcardType ? wildcardType :
10209-
type.flags & TypeFlags.Unknown ? neverType :
10241+
type.flags & (TypeFlags.Unknown | TypeFlags.NominalBrand) ? neverType :
1021010242
type.flags & (TypeFlags.Any | TypeFlags.Never) ? keyofConstraintType :
1021110243
stringsOnly ? !noIndexSignatures && getIndexInfoOfType(type, IndexKind.String) ? stringType : getLiteralTypeFromProperties(type, TypeFlags.StringLiteral) :
1021210244
!noIndexSignatures && getIndexInfoOfType(type, IndexKind.String) ? getUnionType([stringType, numberType, getLiteralTypeFromProperties(type, TypeFlags.UniqueESSymbol)]) :
@@ -10235,9 +10267,12 @@ namespace ts {
1023510267
links.resolvedType = getIndexType(getTypeFromTypeNode(node.type));
1023610268
break;
1023710269
case SyntaxKind.UniqueKeyword:
10270+
// Note, the first case does _not_ unwrap parenthesis - this is intentional
10271+
// This means if you _actually want_ a _class_ of nominally branded symbols, you can
10272+
// accomplish as much by writing `unique (symbol)` to avoid getting a symbol-tied-to-a-value
1023810273
links.resolvedType = node.type.kind === SyntaxKind.SymbolKeyword
1023910274
? getESSymbolLikeTypeForNode(walkUpParenthesizedTypes(node.parent))
10240-
: errorType;
10275+
: getUniqueBrandOfType(getTypeFromTypeNode(node.type), node.symbol, getAliasSymbolForTypeNode(node));
1024110276
break;
1024210277
case SyntaxKind.ReadonlyKeyword:
1024310278
links.resolvedType = getTypeFromTypeNode(node.type);
@@ -10249,6 +10284,37 @@ namespace ts {
1024910284
return links.resolvedType;
1025010285
}
1025110286

10287+
/**
10288+
* Note that this construction of brands precludes the possibility of a branded `never` - a `unique never` will always just be `never`
10289+
* Meanwhile, a `unique unknown` will produce _just_ the brand. A brand is printed as it's alias, or may be hinted at by `unique <type>`
10290+
* (though that does not specify _which_ instance of `unique` is relevant, just that a brand has been applied!)
10291+
*/
10292+
function getUniqueBrandOfType(type: Type, brand: Symbol, aliasSymbol: Symbol | undefined, aliasTypeArguments: readonly Type[] | undefined = getTypeArgumentsForAliasSymbol(aliasSymbol)) {
10293+
const brandType = getOrCreateBrandFromSymbol(brand);
10294+
if (type.flags & TypeFlags.AnyOrUnknown) {
10295+
// A `unique any` or a `unique unknown` both become just the brand type, which needs an alias symbol to be printable
10296+
// If a type like `type MyObj = { x: unique unknown }` is made, the type of `x` is _just_ the brand from the object literal
10297+
// in such scenarios, all printback will simply be `unique unknown` and (if possible), a related span/link back to the `unique` keyword
10298+
// where the type originated
10299+
// Do note that this means `unique any` actually becomes a very restrictive type, because the `unique` brand removes the any-ness - this
10300+
// is intentional. If you're opting in to nominal types, you _probably_ didn't want an `any` flowing in to destroy your brands.
10301+
brandType.aliasSymbol = aliasSymbol;
10302+
brandType.aliasTypeArguments = aliasTypeArguments;
10303+
return brandType;
10304+
}
10305+
return getIntersectionType([type, brandType], aliasSymbol, aliasTypeArguments);
10306+
}
10307+
10308+
function getOrCreateBrandFromSymbol(symbol: Symbol) {
10309+
const links = getSymbolLinks(symbol);
10310+
if (!links.brandType) {
10311+
const brandType = <NominalBrandType>createType(TypeFlags.NominalBrand);
10312+
brandType.symbol = symbol;
10313+
links.brandType = brandType;
10314+
}
10315+
return links.brandType;
10316+
}
10317+
1025210318
function createIndexedAccessType(objectType: Type, indexType: Type) {
1025310319
const type = <IndexedAccessType>createType(TypeFlags.IndexedAccess);
1025410320
type.objectType = objectType;
@@ -10297,6 +10363,9 @@ namespace ts {
1029710363
}
1029810364

1029910365
function getPropertyTypeForIndexType(originalObjectType: Type, objectType: Type, indexType: Type, fullIndexType: Type, suppressNoImplicitAnyError: boolean, accessNode: ElementAccessExpression | IndexedAccessTypeNode | PropertyName | BindingName | SyntheticExpression | undefined, accessFlags: AccessFlags) {
10366+
if (originalObjectType.flags & TypeFlags.NominalBrand) {
10367+
return accessFlags & AccessFlags.Writing ? unknownType : neverType;
10368+
}
1030010369
const accessExpression = accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression ? accessNode : undefined;
1030110370
const propName = getPropertyNameFromIndex(indexType, accessNode);
1030210371
if (propName !== undefined) {
@@ -33169,40 +33238,39 @@ namespace ts {
3316933238

3317033239
function checkGrammarTypeOperatorNode(node: TypeOperatorNode) {
3317133240
if (node.operator === SyntaxKind.UniqueKeyword) {
33172-
if (node.type.kind !== SyntaxKind.SymbolKeyword) {
33173-
return grammarErrorOnNode(node.type, Diagnostics._0_expected, tokenToString(SyntaxKind.SymbolKeyword));
33174-
}
33175-
33176-
const parent = walkUpParenthesizedTypes(node.parent);
33177-
switch (parent.kind) {
33178-
case SyntaxKind.VariableDeclaration:
33179-
const decl = parent as VariableDeclaration;
33180-
if (decl.name.kind !== SyntaxKind.Identifier) {
33181-
return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_may_not_be_used_on_a_variable_declaration_with_a_binding_name);
33182-
}
33183-
if (!isVariableDeclarationInVariableStatement(decl)) {
33184-
return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_are_only_allowed_on_variables_in_a_variable_statement);
33185-
}
33186-
if (!(decl.parent.flags & NodeFlags.Const)) {
33187-
return grammarErrorOnNode((<VariableDeclaration>parent).name, Diagnostics.A_variable_whose_type_is_a_unique_symbol_type_must_be_const);
33188-
}
33189-
break;
33241+
if (node.type.kind === SyntaxKind.SymbolKeyword) {
33242+
// Continue to recognize `unique symbol` as a special kind of type with certain restrictions to permit analysis
33243+
const parent = walkUpParenthesizedTypes(node.parent);
33244+
switch (parent.kind) {
33245+
case SyntaxKind.VariableDeclaration:
33246+
const decl = parent as VariableDeclaration;
33247+
if (decl.name.kind !== SyntaxKind.Identifier) {
33248+
return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_may_not_be_used_on_a_variable_declaration_with_a_binding_name);
33249+
}
33250+
if (!isVariableDeclarationInVariableStatement(decl)) {
33251+
return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_are_only_allowed_on_variables_in_a_variable_statement);
33252+
}
33253+
if (!(decl.parent.flags & NodeFlags.Const)) {
33254+
return grammarErrorOnNode((<VariableDeclaration>parent).name, Diagnostics.A_variable_whose_type_is_a_unique_symbol_type_must_be_const);
33255+
}
33256+
break;
3319033257

33191-
case SyntaxKind.PropertyDeclaration:
33192-
if (!hasModifier(parent, ModifierFlags.Static) ||
33193-
!hasModifier(parent, ModifierFlags.Readonly)) {
33194-
return grammarErrorOnNode((<PropertyDeclaration>parent).name, Diagnostics.A_property_of_a_class_whose_type_is_a_unique_symbol_type_must_be_both_static_and_readonly);
33195-
}
33196-
break;
33258+
case SyntaxKind.PropertyDeclaration:
33259+
if (!hasModifier(parent, ModifierFlags.Static) ||
33260+
!hasModifier(parent, ModifierFlags.Readonly)) {
33261+
return grammarErrorOnNode((<PropertyDeclaration>parent).name, Diagnostics.A_property_of_a_class_whose_type_is_a_unique_symbol_type_must_be_both_static_and_readonly);
33262+
}
33263+
break;
3319733264

33198-
case SyntaxKind.PropertySignature:
33199-
if (!hasModifier(parent, ModifierFlags.Readonly)) {
33200-
return grammarErrorOnNode((<PropertySignature>parent).name, Diagnostics.A_property_of_an_interface_or_type_literal_whose_type_is_a_unique_symbol_type_must_be_readonly);
33201-
}
33202-
break;
33265+
case SyntaxKind.PropertySignature:
33266+
if (!hasModifier(parent, ModifierFlags.Readonly)) {
33267+
return grammarErrorOnNode((<PropertySignature>parent).name, Diagnostics.A_property_of_an_interface_or_type_literal_whose_type_is_a_unique_symbol_type_must_be_readonly);
33268+
}
33269+
break;
3320333270

33204-
default:
33205-
return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_are_not_allowed_here);
33271+
default:
33272+
return grammarErrorOnNode(node, Diagnostics.unique_symbol_types_are_not_allowed_here);
33273+
}
3320633274
}
3320733275
}
3320833276
else if (node.operator === SyntaxKind.ReadonlyKeyword) {

0 commit comments

Comments
 (0)