diff --git a/CHANGELOG.md b/CHANGELOG.md index 212ab3d..d4271be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 3.21.0 + +**Features:** + +* added `getWellKnownSymbolPropertyOfType` to reliably get symbol named properties due to changes in typescript@4.3 +* `getPropertyNameOfWellKnownSymbol` is now deprecated + # 3.20.0 **Features:** diff --git a/package.json b/package.json index 60dee64..1bd9dbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tsutils", - "version": "3.20.0", + "version": "3.21.0", "description": "utilities for working with typescript's AST", "scripts": { "precompile": "rimraf \"{,util,typeguard,test{,/rules}/*.{js,d.ts,js.map}\"", diff --git a/util/type.ts b/util/type.ts index 87bf876..65cb3c1 100644 --- a/util/type.ts +++ b/util/type.ts @@ -30,6 +30,8 @@ import { isShorthandPropertyAssignment, isEnumMember, isClassLikeDeclaration, + isInterfaceDeclaration, + isSourceFile, } from '../typeguard/node'; export function isEmptyObjectType(type: ts.Type): type is ts.ObjectType { @@ -205,6 +207,29 @@ export function getPropertyOfType(type: ts.Type, name: ts.__String) { return type.getProperties().find((s) => s.escapedName === name); } +export function getWellKnownSymbolPropertyOfType(type: ts.Type, wellKnownSymbolName: string, checker: ts.TypeChecker) { + const prefix = '__@' + wellKnownSymbolName; + for (const prop of type.getProperties()) { + if (!prop.name.startsWith(prefix)) + continue; + const globalSymbol = checker.getApparentType( + checker.getTypeAtLocation(((prop.valueDeclaration).name).expression), + ).symbol; + if (prop.escapedName === getPropertyNameOfWellKnownSymbol(checker, globalSymbol, wellKnownSymbolName)) + return prop; + } + return; +} + +function getPropertyNameOfWellKnownSymbol(checker: ts.TypeChecker, symbolConstructor: ts.Symbol | undefined, symbolName: string) { + const knownSymbol = symbolConstructor && + checker.getTypeOfSymbolAtLocation(symbolConstructor, symbolConstructor.valueDeclaration).getProperty(symbolName); + const knownSymbolType = knownSymbol && checker.getTypeOfSymbolAtLocation(knownSymbol, knownSymbol.valueDeclaration); + if (knownSymbolType && isUniqueESSymbolType(knownSymbolType)) + return knownSymbolType.escapedName; + return ('__@' + symbolName); +} + /** Determines if writing to a certain property of a given type is allowed. */ export function isPropertyReadonlyInType(type: ts.Type, name: ts.__String, checker: ts.TypeChecker): boolean { let seenProperty = false; @@ -285,11 +310,26 @@ export function getPropertyNameFromType(type: ts.Type): PropertyName | undefined } if (isUniqueESSymbolType(type)) return { - displayName: `[${type.symbol ? type.symbol.name : (type.escapedName).replace(/^__@|@\d+$/g, '')}]`, + displayName: `[${type.symbol + ? `${isKnownSymbol(type.symbol) ? 'Symbol.' : ''}${type.symbol.name}` + : (type.escapedName).replace(/^__@|@\d+$/g, '') + }]`, symbolName: type.escapedName, }; } +function isKnownSymbol(symbol: ts.Symbol): boolean { + return isSymbolFlagSet(symbol, ts.SymbolFlags.Property) && + symbol.valueDeclaration !== undefined && + isInterfaceDeclaration(symbol.valueDeclaration.parent) && + symbol.valueDeclaration.parent.name.text === 'SymbolConstructor' && + isGlobalDeclaration(symbol.valueDeclaration.parent); +} + +function isGlobalDeclaration(node: ts.DeclarationStatement): boolean { + return isNodeFlagSet(node.parent!, ts.NodeFlags.GlobalAugmentation) || isSourceFile(node.parent) && !ts.isExternalModule(node.parent); +} + export function getSymbolOfClassLikeDeclaration(node: ts.ClassLikeDeclaration, checker: ts.TypeChecker) { return checker.getSymbolAtLocation(node.name ?? getChildOfKind(node, ts.SyntaxKind.ClassKeyword)!)!; } diff --git a/util/util.ts b/util/util.ts index b900645..5b4d92c 100644 --- a/util/util.ts +++ b/util/util.ts @@ -1647,6 +1647,7 @@ export interface PropertyName { symbolName: ts.__String; } +/** @deprecated typescript 4.3 removed the concept of literal well known symbols. Use `getPropertyNameFromType` instead. */ export function getPropertyNameOfWellKnownSymbol(node: WellKnownSymbolLiteral): PropertyName { return { displayName: `[Symbol.${node.name.text}]`, @@ -1654,6 +1655,8 @@ export function getPropertyNameOfWellKnownSymbol(node: WellKnownSymbolLiteral): }; } +const isTsBefore43 = (([major, minor]) => major < '4' || major === '4' && minor < '3')(ts.versionMajorMinor.split('.')); + export interface LateBoundPropertyNames { /** Whether all constituents are literal names. */ known: boolean; @@ -1667,8 +1670,8 @@ export function getLateBoundPropertyNames(node: ts.Expression, checker: ts.TypeC }; node = unwrapParentheses(node); - if (isWellKnownSymbolLiterally(node)) { - result.names.push(getPropertyNameOfWellKnownSymbol(node)); + if (isTsBefore43 && isWellKnownSymbolLiterally(node)) { + result.names.push(getPropertyNameOfWellKnownSymbol(node)); // wotan-disable-line no-unstable-api-use } else { const type = checker.getTypeAtLocation(node); for (const key of unionTypeParts(checker.getBaseConstraintOfType(type) || type)) { @@ -1700,8 +1703,8 @@ export function getSingleLateBoundPropertyNameOfPropertyName(node: ts.PropertyNa if (node.kind === ts.SyntaxKind.PrivateIdentifier) return {displayName: node.text, symbolName: checker.getSymbolAtLocation(node)!.escapedName}; const {expression} = node; - return isWellKnownSymbolLiterally(expression) - ? getPropertyNameOfWellKnownSymbol(expression) + return isTsBefore43 && isWellKnownSymbolLiterally(expression) + ? getPropertyNameOfWellKnownSymbol(expression) // wotan-disable-line no-unstable-api-use : getPropertyNameFromType(checker.getTypeAtLocation(expression)); }