typescript-extract-shared.ts

Shared utilities for the per-declaration extractors in typescript-extract-*.ts.

Holds helpers used across function, type, and class extraction: signature parameter extraction, overload detection, generic parsing, modifier extraction, location reporting, intersection-property filtering, and runes detection.

@see typescript-extract-function.ts, typescript-extract-type.ts, typescript-extract-class.ts for the per-declaration extractors that build on these helpers

view source

Declarations
#

23 declarations

applyHeritageExternalTypes
#

typescript-extract-shared.ts view source

(declaration: DeclarationJsonBuild, heritageTypes: readonly ExpressionWithTypeArguments[], ctx: ExtractContext): void import {applyHeritageExternalTypes} from 'svelte-docinfo/typescript-extract-shared.js';

Set a declaration's externalTypes from its own heritage entries — the interface/class counterpart of filterDocumentedProperties' annotation walk, and the one place the two heritage-bearing extractors express it.

Interfaces and classes enumerate own members only, so nothing is *filtered* — what the field records there is the external types the heritage composition reaches whose contributions members therefore never enumerates: interface Props extends HTMLButtonAttributes records the bag, directly or through a local base chain, exactly as the same interface annotating $props() records it on the component. Local bases reached and fully enumerated nowhere are the extends field's business, not this one.

Only extends entries are passed by either caller: an interface has no other clause kind, and a class's implements adds no members.

declaration

heritageTypes

type readonly ExpressionWithTypeArguments[]

ctx

returns

void

mutates

  • declaration — sets `externalTypes` when the walk finds any, left absent when not

createExtractContext
#

typescript-extract-shared.ts view source

(program: Program, checker: TypeChecker, options: ModuleSourceOptions, diagnostics: ({ symbolName: string; file: string; message: string; severity: "error" | "warning"; kind: "type_extraction_failed"; line?: number | undefined; column?: number | undefined; } | ... 14 more ... | { ...; })[], aliasRegistry: AliasRegistry | undefined): ExtractContext import {createExtractContext} from 'svelte-docinfo/typescript-extract-shared.js';

The module dispatchers' ExtractContext construction — analyzeModule and analyzeSvelteModule are where the program is in hand, and both want the same answers from it. One owner so the derived fields can't drift or be forgotten by a third dispatcher: isExternalFile comes from the very options the pass will be handed (analyzeExports derives its own isExternalPath from that object, and the two axes must agree), and exactOptionalPropertyTypes is read off the *merged* compiler options rather than the raw tsconfig, so a session-level override reaches the strip gate (optionalWidened).

Direct callers outside the dispatchers (tests, fixture harnesses, subpath consumers) build the object literal themselves and decide every field explicitly — see mockExtractContext in src/test/test-helpers.ts.

program

type Program

checker

type TypeChecker

options

diagnostics

type ({ symbolName: string; file: string; message: string; severity: "error" | "warning"; kind: "type_extraction_failed"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 13 more ... | { ...; })[]

aliasRegistry

type AliasRegistry | undefined

returns

ExtractContext

detectReactivity
#

typescript-extract-shared.ts view source

(initializer: Expression | undefined): "$state" | "$state.raw" | "$derived" | "$derived.by" | undefined import {detectReactivity} from 'svelte-docinfo/typescript-extract-shared.js';

Detect a Svelte 5 reactivity rune from a variable or property initializer.

Inspects the AST since runes erase to their inner type after type-checking. Returns undefined for any non-rune expression. See the Reactivity enum in types.ts for the rationale on running this on every file regardless of extension.

initializer

type Expression | undefined

returns

"$state" | "$state.raw" | "$derived" | "$derived.by" | undefined

emitCallOrConstructSignature
#

typescript-extract-shared.ts view source

(getSignatures: () => readonly Signature[], signatureKind: "call" | "construct", resolveTsdocNode: (sig: Signature) => Node | undefined, paramValidationFallbackNode: Node, declaration: DeclarationJsonBuild, ctx: ExtractContext, errorContext: { ...; }): void import {emitCallOrConstructSignature} from 'svelte-docinfo/typescript-extract-shared.js';

Append a (call) or (construct) signature member to a declaration.

Captures the extraction logic shared by interface processing (extractTypeInfo) and type-alias property processing (extractTypeAliasProperties): type signature, parameters, generics, overloads, and TSDoc. The TSDoc source node is supplied by the caller — interfaces look it up in node.members (skipping TSDoc when no inline signature is declared, even if one is inherited), type aliases use sig.getDeclaration().

getSignatures

thunk to retrieve getCallSignatures() / getConstructSignatures(); called inside the try so checker errors are captured as diagnostics

type () => readonly Signature[]

signatureKind

'call' (member kind: function, includes returnType) or 'construct' (member kind: constructor, no returnType)

type "call" | "construct"

resolveTsdocNode

callback returning the AST node to parse TSDoc from, or undefined to skip TSDoc resolution

type (sig: Signature) => Node | undefined

paramValidationFallbackNode

location used by validateParamKeys when resolveTsdocNode returns undefined

type Node

declaration

parent declaration (mutated; appended to members)

ctx

errorContext

type { node: Node; kindLabel: string; }

returns

void

mutates

  • declaration — appends a member when signatures are present;

ExtractContext
#

typescript-extract-shared.ts view source

ExtractContext import type {ExtractContext} from 'svelte-docinfo/typescript-extract-shared.js';

The pass-scoped, cross-cutting state one extraction run threads through the extractor seams — analyzeDeclaration down to the resolveTypeInfo call sites. Constructed once per module walk (analyzeExports, analyzeSvelteModule) or per direct call (tests, fixture harnesses).

Membership is deliberately tight: only state that is constant for the pass and consumed across extractor boundaries belongs here. Per-declaration inputs (nodes, symbols, parsed TSDoc, written annotations, names) stay positional parameters.

checker

type TypeChecker

diagnostics

Accumulator for non-fatal issues — mutated via Array.push throughout the pass.

type ({ symbolName: string; file: string; message: string; severity: "error" | "warning"; kind: "type_extraction_failed"; line?: number | undefined; column?: number | undefined; } | { functionName: string; ... 5 more ...; column?: number | undefined; } | ... 13 more ... | { ...; })[]

isExternalFile

Predicate for external source files (node_modules, out-of-tree declarations).

type (sourceFile: SourceFile): boolean

sourceFile

type SourceFile
returns boolean

aliasRegistry

Identity-keyed registry of the analyzed set's exported alias-lost type aliases (buildAliasRegistry in typescript-alias-registry.ts), or undefined when no pre-pass ran — registry recovery is then disabled while written-name recovery keeps working. A required field so every construction site decides explicitly.

type AliasRegistry | undefined

exactOptionalPropertyTypes

The program's exactOptionalPropertyTypes compiler option — read it off program.getCompilerOptions(). Consumed by optionalWidened, which documents the strip-gating policy. A required field so every construction site decides explicitly, like aliasRegistry.

type boolean

extractModifiers
#

typescript-extract-shared.ts view source

(modifiers: readonly ModifierLike[] | undefined): ("public" | "protected" | "readonly" | "static" | "abstract" | "getter" | "setter")[] import {extractModifiers} from 'svelte-docinfo/typescript-extract-shared.js';

Extract modifier keywords from a node's modifiers.

Returns an array of modifier strings like ['public', 'readonly', 'static'].

modifiers

type readonly ModifierLike[] | undefined

returns

("public" | "protected" | "readonly" | "static" | "abstract" | "getter" | "setter")[]

extractSignatureParameters
#

typescript-extract-shared.ts view source

(sig: Signature, ctx: ExtractContext, tsdocParams: Record<string, string> | undefined): { name: string; type: string; optional: boolean; rest: boolean; typeInfo?: TypeJson | undefined; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[] import {extractSignatureParameters} from 'svelte-docinfo/typescript-extract-shared.js';

Extract parameters from a TypeScript signature with TSDoc descriptions and default values.

Shared helper for extracting parameter information from both standalone functions and class methods/constructors.

sig

the TypeScript signature to extract parameters from

type Signature

ctx

the extraction pass's context (checker + alias registry)

tsdocParams

record of parameter names to TSDoc descriptions (from TsdocParsedComment.params)

type Record<string, string> | undefined

returns

{ name: string; type: string; optional: boolean; rest: boolean; typeInfo?: TypeJson | undefined; description?: string | undefined; defaultValue?: string | undefined; propertyDescriptions?: Record<...> | undefined; }[]

array of ParameterJson objects

filterDocumentedProperties
#

typescript-extract-shared.ts view source

(type: Type, typeNode: Node, checker: TypeChecker, isExternalFile: IsExternalFile): { properties: Symbol[]; externalTypes: string[]; } import {filterDocumentedProperties} from 'svelte-docinfo/typescript-extract-shared.js';

Reduce a type's properties to the ones consumers can see, and collect the external type references that contributed the dropped ones.

Two axes drop a property, both by declaration:

  • Origin (isExternalProperty) — the property comes from node_modules or a declaration file. Structure-agnostic: TypeScript preserves original declaration sources on derived properties, so the test gives the right answer through utility-type wrappers (Partial, Pick, OmitStrict) too. A property with no declarations (synthesized) is treated as local and kept.
  • Visibility (isPrivateProperty) — the property is private or a # field of a class this type projects. Only classes can declare either, so this is a no-op on every other shape; it exists because a class *type* reaches here whenever a structural container names one (`type X = LocalClass, type X = LocalGen<string>`), and what the class's own declaration hides an alias over it must hide too.

Applies to any composition shape — intersection, union, bare reference, indexed-access — not only intersections. The labels naming the dropped external contributors come from an AST walk (collectExternalTypeRefs) — the authoritative source for the &/|/index-access shape inference would otherwise erase — which descends through project-local names so a bag inherited via interface Props extends Bag is labeled like an inline Bag & {…} branch. Private properties are dropped silently: they are the project's own code, with nothing to attribute.

type

type Type

typeNode

type Node

checker

type TypeChecker

isExternalFile

returns

{ properties: Symbol[]; externalTypes: string[]; }

getLocalExportStatement
#

typescript-extract-shared.ts view source

(exportSymbol: Symbol, sourceFile: SourceFile): { node: ExportSpecifier | NamespaceExport; statement: ExportDeclaration; } | undefined import {getLocalExportStatement} from 'svelte-docinfo/typescript-extract-shared.js';

The local export statement and binding node for an alias export symbol — {node, statement} where node is the ExportSpecifier (or, for export * as ns, the NamespaceExport) and statement its ExportDeclaration.

Returns undefined when the statement isn't in sourceFile: merged symbols can put a foreign declaration first, and parsing JSDoc or positions there would attribute another module's content here.

exportSymbol

type Symbol

sourceFile

type SourceFile

returns

{ node: ExportSpecifier | NamespaceExport; statement: ExportDeclaration; } | undefined

getNodeLocation
#

typescript-extract-shared.ts view source

(node: Node): { file: string; line: number; column: number; } import {getNodeLocation} from 'svelte-docinfo/typescript-extract-shared.js';

Extract line and column from a TypeScript node. Returns 1-based line and column numbers.

node

type Node

returns

{ file: string; line: number; column: number; }

getNonOptionalType
#

typescript-extract-shared.ts view source

(type: Type, checker: TypeChecker, optional: boolean): Type import {getNonOptionalType} from 'svelte-docinfo/typescript-extract-shared.js';

The type of an optional declaration with the widening undefined member removed — the counterpart to getTypeSignature's optional strip for structural queries, taking the position's optionality the same way (false is identity).

A union with undefined reports no call signatures of its own, so under strictNullChecks an optional method (fn?(a: string): number) or function-typed property resolves to ((a: string) => number) | undefined and reads as non-callable, silently costing it typeSignature, parameters, and returnType. Analogous to getNonNullableType, but leaves null in place: fn?: (() => void) | null really isn't callable, and reporting it as a function would hide the null.

A null-free union goes through getNonNullableType, which rebuilds the union rather than picking a member — so a union of callables (fn?: (() => void) | (() => number)) keeps its combined call signature. A null-bearing union is returned unchanged: null poisons callability regardless, so there's nothing to recover by stripping undefined from it.

The selection itself is optionalWideningTarget's — the same owner getTypeSignature and the TypeJson builder select through, so the structural queries can't drift from the printed and structured outputs.

Every callability query routes through this with the member's own optionality, in both widening modes — the property site (populatePropertyMember) and the method sites (interface and class) alike. At an optional position an undefined member — widened or written — is the same runtime observation as absence, already carried by optional: true, so under exactOptionalPropertyTypes a written fn?: (() => void) | undefined classifies callable like fn?: () => void (that spelling is often forced there — assigning a possibly-undefined handler to the property requires it). | null stays demoted per the paragraph above — null is a real value absence doesn't imply. The printed and structured outputs still gate their strips on optionalWidened, where a written undefined is content. On the method sites the strip is identity under the flag anyway — method syntax can't write | undefined.

type

type Type

checker

type TypeChecker

optional

type boolean

returns

Type

getTypeSignature
#

typescript-extract-shared.ts view source

(type: Type, checker: TypeChecker, optional: boolean): string import {getTypeSignature} from 'svelte-docinfo/typescript-extract-shared.js';

Type signature of a declaration — for an optional one, with the implicit | undefined widening removed. The single chokepoint pairing optional with the strip, so a call site can't apply one without the other. The target selection itself lives in optionalWideningTarget (in typescript-extract-type-json.ts), shared with the TypeJson builder so the flat string and the tree can't drift.

The checker widens every optional property and parameter to include undefined, which is redundant with optional: true in the output. Removing it with checker.getNonNullableType drops null too, so x?: string | null printed as "string" and x?: null as "never" — both silently wrong. Filtering undefined out of type.types and rejoining the members is no better: it loses the alias name, the printer's member order, and the parens that keep function members legal.

So null-bearing unions are trimmed on the printed union instead, where TypeScript always emits a top-level undefined last — truncated unions ('a' | ... 11 more ... | undefined) included. Every other union keeps taking getNonNullableType, which prints an optional function type without the union parens the trim would leave behind (() => void, not (() => void)).

Applies to the structured fields only. A callable's typeSignature comes from checker.signatureToString, which has no flag to omit the widening, so it renders optional parameters as the checker does — (a?: number | undefined): void.

A non-union optional is printed as written, since there's no widening member to remove: x?: undefined stays "undefined" (stripping would leave never, the same silent-wrong shape as the null cases above), and x?: unknown stays "unknown" (getNonNullableType would answer {}). See optionalWideningTarget for the full case split.

Under exactOptionalPropertyTypes the checker doesn't widen optional properties at all, so property sites gate the strip off via optionalWidened (passing false here) — every undefined there is author-written (x?: T | undefined is a distinct type from x?: T in that mode) and stripping would trim it. Parameter and tuple-element sites pass optional unconditionally: the flag governs properties only, and both keep widening under it.

type

type Type

checker

type TypeChecker

optional

type boolean

returns

string

inferDeclarationKind
#

typescript-extract-shared.ts view source

(symbol: Symbol, node: Node): "function" | "type" | "variable" | "class" | "interface" | "enum" | "component" | "snippet" | "namespace" import {inferDeclarationKind} from 'svelte-docinfo/typescript-extract-shared.js';

Infer declaration kind from symbol and node.

Maps TypeScript constructs to DeclarationKind:

  • Classes → 'class'
  • Functions (declarations, expressions, arrows) → 'function'
  • Interfaces → 'interface'
  • Type aliases → 'type'
  • Enums (regular and const) → 'enum'
  • Variables → 'variable' (unless function-valued → 'function')

Note: namespace re-exports (export * as ns from './x') have no inline declaration form in TypeScript and are caught upstream in analyzeExports via classifyNamespaceReExport. They never reach this function. A direct call here on a ValueModule symbol would fall through to 'variable' and leak typeof import("/abs/path") into the output — keep the namespace dispatch in analyzeExports.

symbol

type Symbol

node

type Node

returns

"function" | "type" | "variable" | "class" | "interface" | "enum" | "component" | "snippet" | "namespace"

isDeclaredInFile
#

typescript-extract-shared.ts view source

(symbol: Symbol, fileName: string): boolean import {isDeclaredInFile} from 'svelte-docinfo/typescript-extract-shared.js';

Whether any of the symbol's declarations lives in fileName (virtual-suffix-normalized).

Merged symbols (module augmentation, declaration merging) can have declarations in several files — a symbol counts as declared in the file when at least one declaration is, so checking a single declaration node would drop locally-declared exports depending on bind order. Symbols without declarations are treated as declared in the file (permissive).

symbol

type Symbol

fileName

type string

returns

boolean

isExternalIndexInfo
#

typescript-extract-shared.ts view source

(info: IndexInfo, isExternalFile: IsExternalFile): boolean import {isExternalIndexInfo} from 'svelte-docinfo/typescript-extract-shared.js';

Check whether an index signature comes from an external file.

The checker synthesizes index infos for every mapped-type instantiation (Record<string, X>, Partial<Indexed>, hand-written {[K in string]: X}) with no declaration, so those read local under originIsExternal's fail-open, while a bare reference or an inherited info preserves the original index-signature declaration, external file included.

info

type IndexInfo

isExternalFile

returns

boolean

isExternalSignature
#

typescript-extract-shared.ts view source

(sig: Signature, isExternalFile: IsExternalFile): boolean import {isExternalSignature} from 'svelte-docinfo/typescript-extract-shared.js';

Check whether a call/construct signature comes from an external file. In practice a signature always carries its original declaration — through intersections, inheritance, and generic instantiation alike — so the fail-open arm is unreachable for the shapes that occur.

sig

type Signature

isExternalFile

returns

boolean

isPrivateMemberDeclaration
#

typescript-extract-shared.ts view source

(decl: Declaration): boolean import {isPrivateMemberDeclaration} from 'svelte-docinfo/typescript-extract-shared.js';

Check whether a member declaration is private to the class that declares it — a # private identifier name, or a private modifier.

The one visibility rule, shared by the two ways a class's members reach output: extractClassInfo walking node.members at the class's own declaration, and filterDocumentedProperties projecting a class *type* at a structural container (type X = LocalClass, and since generic instantiations extract, type X = LocalGen<string>). Held in one place because the two paths disagreeing is what let # fields reach members through an alias while the class itself dropped them.

protected is deliberately not private: it is part of the extension API a subclass author documents against. Only classes can declare either form — interfaces and type literals have no private members — so this is a no-op on every other shape.

decl

type Declaration

returns

boolean

memberNameText
#

typescript-extract-shared.ts view source

(name: PropertyName): string | undefined import {memberNameText} from 'svelte-docinfo/typescript-extract-shared.js';

The output name for a member's property-name node: the unquoted text of an identifier or string/numeric literal (matching the symbol-based paths, where prop.getName() yields data-foo for a written 'data-foo'), or undefined for computed names (runtime-dependent; the symbol paths skip their __@-prefixed forms too).

name

type PropertyName

returns

string | undefined

optionalWidened
#

typescript-extract-shared.ts view source

(ctx: ExtractContext, optional: boolean): boolean import {optionalWidened} from 'svelte-docinfo/typescript-extract-shared.js';

Whether the checker widened this optional *property* position with undefined — i.e. whether the optional-widening strip applies. Under exactOptionalPropertyTypes the checker doesn't widen optional properties at all, so every undefined in the type is author-written and stripping would corrupt it: x?: T | undefined trimmed to T, `x?: T | null | undefined to T | null`, and a null-free multi-member union rebuilt through getNonNullableType (x?: E | F printed as (E & {}) | (F & {})).

Property sites only — component props, type-alias and interface properties, class properties. Optional *parameters* and optional *tuple elements* widen under both modes (probed on TS 5.9), so their call sites pass optional unconditionally and never route through this gate. populatePropertyMember's callability query also bypasses it — an optional callable strips in both modes (see getNonOptionalType).

ctx

optional

type boolean

returns

boolean

parseGenericParam
#

typescript-extract-shared.ts view source

(param: TypeParameterDeclaration): { name: string; constraint?: string | undefined; defaultType?: string | undefined; } import {parseGenericParam} from 'svelte-docinfo/typescript-extract-shared.js';

Parse a TypeScript generic type parameter declaration into structured info.

param

the TypeScript type parameter declaration node

type TypeParameterDeclaration

returns

{ name: string; constraint?: string | undefined; defaultType?: string | undefined; }

structured GenericParamJson with name, constraint, and default type

populateCallableMember
#

typescript-extract-shared.ts view source

(target: MemberJsonBuild | DeclarationJsonBuild, signatures: readonly Signature[], ctx: ExtractContext, tsdoc: TsdocParsedComment | undefined, paramValidationNode: Node, name: string, includeReturn?: boolean): void import {populateCallableMember} from 'svelte-docinfo/typescript-extract-shared.js';

Populate the callable fields of a declaration or member from its call/construct signatures: typeSignature, parameters, overloads, and (unless includeReturn is false) returnType / returnTypeInfo / returnDescription.

The shared core of every named-callable extractor — standalone functions, interface methods, class methods/constructors, and type-alias function properties. Callers differ in how they obtain signatures (symbol type, constructor declarations, property call signatures) and in their own try/catch + diagnostic kind, so those stay at the callsite; this captures only the identical projection from a resolved signature list onto the build target. No-op when signatures is empty.

target

declaration or member build object (mutated)

type MemberJsonBuild | DeclarationJsonBuild

signatures

public call/construct signatures (signatures[0] is primary)

type readonly Signature[]

ctx

the extraction pass's context

tsdoc

parsed TSDoc for the target (supplies @param/@returns)

type TsdocParsedComment | undefined

paramValidationNode

node validateParamKeys reports unknown_param against

type Node

name

target name, for diagnostic messages

type string

includeReturn

set false for constructors (no return type/description)

type boolean
default true

returns

void

mutates

  • target — sets typeSignature, parameters, overloads, returnType, returnTypeInfo, returnDescription

populatePropertyMember
#

typescript-extract-shared.ts view source

(member: MemberJsonBuild, propType: Type, ctx: ExtractContext, optional: boolean, tsdoc: TsdocParsedComment | undefined, paramValidationNode: Node, name: string, annotation: TypeNode | undefined): void import {populatePropertyMember} from 'svelte-docinfo/typescript-extract-shared.js';

Populate a property-shaped member from its checker type: a callable property becomes kind: 'function' with the full signature field set (generic signatures carry genericParams like method signatures do), everything else gets the flat/structured pair (typeSignature + typeInfo) with the optional-widening strip paired to optional like every checker-backed site. Callability counts local call signatures only (isExternalSignature) — a property typed by an external function documents as the flat type text rather than enumerating the external package's overloads and docs. TSDoc applies here after the projection; members carry @defaultdefaultValue whatever kind the classification settles on (for a callable member it documents the behavior used when the callback is omitted).

The one projection shared by the structural property sites — type-alias properties and interface property signatures — so the same written shape can't extract differently across the two container kinds. Class fields deliberately don't route here: a field holding a function is still a field (kind: 'variable'), while on the structural containers callability is the member's classification.

member

propType

type Type

ctx

optional

type boolean

tsdoc

type TsdocParsedComment | undefined

paramValidationNode

type Node

name

type string

annotation

the written type annotation, when one exists (feeds typeInfo name recovery)

type TypeNode | undefined

returns

void

mutates

  • member — sets kind, doc fields, and either the callable field set or typeSignature/typeInfo

selectDeclarationNode
#

typescript-extract-shared.ts view source

(symbol: Symbol): Declaration | undefined import {selectDeclarationNode} from 'svelte-docinfo/typescript-extract-shared.js';

Select the declaration node that carries a symbol's documented meaning.

Mirrors inferDeclarationKind's flag priority. A symbol can merge value and type meanings — const Foo = ... + type Foo = ... (the schema/type pattern) or const Foo + interface Foo share one symbol with combined flags — and there valueDeclaration points at the value while the flags resolve a type-space kind, so value-first selection would document the value's type under the type's kind. When the flags resolve type-space (interface, type, enum), the first matching type-space declaration is selected instead; value-space kinds (and unmerged symbols) keep the valueDeclaration-first selection. The merged value meaning goes undocumented under the one-declaration-per-export-name model — the type is what consumers look up.

symbol

type Symbol

returns

Declaration | undefined

Depends on
#

Imported by
#