Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions tsc/internal/bundled/libs/lib.es5.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1677,6 +1677,16 @@ type Uncapitalize<S extends string> = intrinsic;
*/
type NoInfer<T> = intrinsic;

/**
* A module path string, resolved exactly as an import written in the same
* file would be.
* T is the type of the referenced module, as given by `typeof import(...)`.
*
* The files of a program are fixed before type checking begins, so the
* referenced module must already be part of the program.
*/
type ModuleReference<T> = intrinsic;

/**
* Marker for contextual 'this' type
*/
Expand Down
108 changes: 102 additions & 6 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,14 +355,16 @@ const (
IntrinsicTypeKindCapitalize
IntrinsicTypeKindUncapitalize
IntrinsicTypeKindNoInfer
IntrinsicTypeKindModuleReference
)

var intrinsicTypeKinds = map[string]IntrinsicTypeKind{
"Uppercase": IntrinsicTypeKindUppercase,
"Lowercase": IntrinsicTypeKindLowercase,
"Capitalize": IntrinsicTypeKindCapitalize,
"Uncapitalize": IntrinsicTypeKindUncapitalize,
"NoInfer": IntrinsicTypeKindNoInfer,
"Uppercase": IntrinsicTypeKindUppercase,
"Lowercase": IntrinsicTypeKindLowercase,
"Capitalize": IntrinsicTypeKindCapitalize,
"Uncapitalize": IntrinsicTypeKindUncapitalize,
"NoInfer": IntrinsicTypeKindNoInfer,
"ModuleReference": IntrinsicTypeKindModuleReference,
}

type MappedTypeModifiers uint32
Expand Down Expand Up @@ -563,6 +565,7 @@ type Program interface {
GetImpliedNodeFormatForEmit(sourceFile ast.HasFileName) core.ModuleKind
GetResolvedModule(currentSourceFile ast.HasFileName, moduleReference string, mode core.ResolutionMode) *module.ResolvedModule
GetResolvedModules() map[tspath.Path]module.ModeAwareCache[*module.ResolvedModule]
ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule
GetPackagesMap() map[string]bool
GetSourceFileMetaData(path tspath.Path) ast.SourceFileMetaData
GetJSXRuntimeImportSpecifier(path tspath.Path) (moduleReference string, specifier *ast.Node)
Expand Down Expand Up @@ -9469,6 +9472,14 @@ func (c *Checker) isSignatureApplicable(node *ast.Node, args []*ast.Node, signat
checkArgType = argType
}
effectiveCheckArgumentNode := c.getEffectiveCheckNode(arg)
if moduleType, target := c.getModuleReferenceArgumentTypes(arg, paramType, reportErrors); target != nil {
if moduleType == nil {
// no such module; the diagnostic is reported when reportErrors is set
return false
}

checkArgType, paramType = moduleType, target
}
if !c.checkTypeRelatedToAndOptionallyElaborate(checkArgType, paramType, relation, core.IfElse(reportErrors, effectiveCheckArgumentNode, nil), effectiveCheckArgumentNode, headMessage, diagnosticOutput) {
c.maybeAddMissingAwaitInfo(arg, checkArgType, paramType, relation, reportErrors, diagnosticOutput)
return false
Expand Down Expand Up @@ -9557,6 +9568,71 @@ func (c *Checker) getEffectiveCheckNode(argument *ast.Node) *ast.Node {
return ast.SkipOuterExpressions(argument, flags)
}

func isModuleReferenceType(t *Type) bool {
return t.flags&TypeFlagsStringMapping != 0 && intrinsicTypeKinds[t.symbol.Name] == IntrinsicTypeKindModuleReference
}

// This handles a string literal passed where a `ModuleReference<T>` is expected.
// The literal is resolved as a module specifier exactly as an import written
// in its place would be, so both relative paths and the CommonJS/ESM
// resolution mode follow the call site rather than wherever `ModuleReference`
// was declared. Callers check and infer the referenced module's type
// against T instead of checking the literal against the parameter.
//
// The second result is T, and is nil when this isn't a module reference argument.
// The first is the referenced module's type, and is nil when the specifier names
// no module in the program, in which case the same diagnostic an unresolvable
// import would produce is reported if reportErrors is set.
func (c *Checker) getModuleReferenceArgumentTypes(arg *ast.Node, paramType *Type, reportErrors bool) (moduleType *Type, target *Type) {
if !isModuleReferenceType(paramType) {
return nil, nil
}

specifier := c.getEffectiveCheckNode(arg)

if !ast.IsStringLiteralLike(specifier) {
return nil, nil
}

if moduleSymbol := c.resolveModuleReference(specifier, reportErrors); moduleSymbol != nil {
if resolved := c.resolveExternalModuleSymbol(moduleSymbol, false); resolved != nil {
moduleType = c.getTypeOfSymbol(resolved)
}
}

return moduleType, paramType.AsStringMappingType().target
}

// This resolves the specifier named by a `ModuleReference<T>` argument.
// Module specifiers are collected syntactically and resolved before checking
// begins, and a module reference is not discoverable without type information,
// so its specifier usually has no resolution cached.
// Resolving one here cannot change the set of files in the program.
// The result is used only when it names a file the program already contains,
// which is what keeps compilation phased and its output deterministic.
// https://github.com/microsoft/TypeScript/issues/54022
func (c *Checker) resolveModuleReference(specifier *ast.Node, reportErrors bool) *ast.Symbol {
if moduleSymbol := c.resolveExternalModuleName(specifier, specifier, true, nil); moduleSymbol != nil {
return moduleSymbol
}

file := ast.GetSourceFileOfNode(specifier)
resolved := c.program.ResolveModuleName(specifier.Text(), file.FileName(), c.program.GetModeForUsageLocation(file, specifier))

if resolved.IsResolved() {
if referenced := c.program.GetSourceFileForResolvedModule(resolved.ResolvedFileName); referenced != nil && referenced.Symbol != nil {
return c.getMergedSymbol(referenced.Symbol)
}
}

if reportErrors {
// Report whatever an import of the same specifier would have reported.
c.resolveExternalModuleName(specifier, specifier, false, nil)
}

return nil
}

func (c *Checker) inferTypeArguments(node *ast.Node, signature *Signature, args []*ast.Node, checkMode CheckMode, context *InferenceContext) []*Type {
if ast.IsJsxOpeningLikeElement(node) {
return c.inferJsxTypeArguments(node, signature, checkMode, context)
Expand Down Expand Up @@ -9653,7 +9729,17 @@ func (c *Checker) inferTypeArguments(node *ast.Node, signature *Signature, args
paramType := c.getTypeAtPosition(signature, i)
if c.couldContainTypeVariables(paramType) {
argType := c.checkExpressionWithContextualType(arg, paramType, context, checkMode)
c.inferTypes(context.inferences, argType, paramType, InferencePriorityNone, false)

if moduleType, target := c.getModuleReferenceArgumentTypes(arg, paramType, false); target != nil {
// A specifier naming no module leaves moduleType nil,
// and so makes no inference. The error is reported
// when the signature's applicability is checked.
argType, paramType = moduleType, target
}

if argType != nil {
c.inferTypes(context.inferences, argType, paramType, InferencePriorityNone, false)
}
}
}
}
Expand Down Expand Up @@ -27878,6 +27964,10 @@ func (c *Checker) computeBaseConstraint(t *Type, stack []RecursionId) *Type {
}
return c.stringType
case t.flags&TypeFlagsStringMapping != 0:
if isModuleReferenceType(t) {
return c.stringType
}

constraint := c.getNextBaseConstraint(t.Target(), stack)
if constraint != nil && constraint != t.Target() {
return c.getStringMappingType(t.symbol, constraint)
Expand Down Expand Up @@ -29558,6 +29648,12 @@ func (c *Checker) getTemplateStringForType(t *Type) string {

func (c *Checker) getStringMappingType(symbol *ast.Symbol, t *Type) *Type {
switch {
case intrinsicTypeKinds[symbol.Name] == IntrinsicTypeKindModuleReference:
// Unlike the string mapping intrinsics, a module reference's type
// argument is the type of the referenced module rather than a
// string, so there is nothing to map. The type stays deferred
// until a string literal is checked against it.
return c.getStringMappingTypeForGenericType(symbol, t)
case t.flags&(TypeFlagsUnion|TypeFlagsNever) != 0:
return c.mapType(t, func(t *Type) *Type { return c.getStringMappingType(symbol, t) })
case t.flags&TypeFlagsStringLiteral != 0:
Expand Down
6 changes: 6 additions & 0 deletions tsc/internal/checker/relater.go
Original file line number Diff line number Diff line change
Expand Up @@ -2534,6 +2534,12 @@ func (c *Checker) isMemberOfStringMapping(source *Type, target *Type) bool {
return true
case target.flags&(TypeFlagsString|TypeFlagsTemplateLiteral) != 0:
return c.isTypeAssignableTo(source, target)
case isModuleReferenceType(target):
// Any string literal may name a module. Whether it actually
// resolves is checked where the literal appears, which is
// the only place the containing file, and the meaning of
// a relative specifier, is known.
return source.flags&TypeFlagsStringLiteral != 0
case target.flags&TypeFlagsStringMapping != 0:
// We need to see whether applying the same mappings of the target
// onto the source would produce an identical type *and* that
Expand Down
5 changes: 5 additions & 0 deletions tsc/internal/fourslash/tests/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,11 @@ var CompletionGlobalTypeDecls = []fourslash.CompletionsExpectedItem{
Kind: new(lsproto.CompletionItemKindClass),
SortText: new(string(ls.SortTextGlobalsOrKeywords)),
},
&lsproto.CompletionItem{
Label: "ModuleReference",
Kind: new(lsproto.CompletionItemKindClass),
SortText: new(string(ls.SortTextGlobalsOrKeywords)),
},
&lsproto.CompletionItem{
Label: "ThisType",
Kind: new(lsproto.CompletionItemKindInterface),
Expand Down
6 changes: 6 additions & 0 deletions tsc/internal/ls/autoimport/aliasresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ func (r *aliasResolver) GetResolvedModule(currentSourceFile ast.HasFileName, mod
return resolved
}

func (r *aliasResolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule {
resolved, _ := r.moduleResolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil)

return resolved
}

// GetSourceFileForResolvedModule implements checker.Program.
func (r *aliasResolver) GetSourceFileForResolvedModule(fileName string) *ast.SourceFile {
return r.GetSourceFile(fileName)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
main.ts(9,8): error TS2339: Property 'missing' does not exist on type 'typeof import("mod")'.
main.ts(12,26): error TS2741: Property 'label' is missing in type '{ getRandom: () => number; }' but required in type 'typeof import("mod")'.
main.ts(17,15): error TS2741: Property 'doThing' is missing in type 'typeof import("notAPlugin")' but required in type '{ doThing(): void; }'.
main.ts(18,36): error TS2741: Property 'doThing' is missing in type 'typeof import("notAPlugin")' but required in type '{ doThing(): void; }'.
main.ts(28,16): error TS2307: Cannot find module './nope' or its corresponding type declarations.
main.ts(29,16): error TS2345: Argument of type 'string' is not assignable to parameter of type 'ModuleReference<unknown>'.


==== mod.ts (0 errors) ====
export const getRandom = () => 10;
export const label = "mod";

==== plugin.ts (0 errors) ====
export function doThing(): void {}

==== notAPlugin.ts (0 errors) ====
export function somethingElse(): void {}

==== main.ts (6 errors) ====
declare const jest: {
requireActual<T>(ref: ModuleReference<T>): T;
mock<T>(ref: ModuleReference<T>, factory: () => NoInfer<T>): void;
};

const actual = jest.requireActual("./mod");
actual.getRandom();
actual.label;
actual.missing; // error, no such export
~~~~~~~
!!! error TS2339: Property 'missing' does not exist on type 'typeof import("mod")'.

jest.mock("./mod", () => ({ ...jest.requireActual("./mod"), getRandom: () => 10 }));
jest.mock("./mod", () => ({ getRandom: () => 10 })); // error, 'label' is missing
~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2741: Property 'label' is missing in type '{ getRandom: () => number; }' but required in type 'typeof import("mod")'.
!!! related TS2728 mod.ts:2:14: 'label' is declared here.
!!! related TS6502 main.ts:3:47: The expected type comes from the return type of this signature.

// The referenced module is checked against the type parameter's constraint.
declare function requirePlugin<T extends { doThing(): void }>(ref: ModuleReference<T>): Promise<T>;
requirePlugin("./plugin");
requirePlugin("./notAPlugin"); // error
~~~~~~~~~~~~~~
!!! error TS2741: Property 'doThing' is missing in type 'typeof import("notAPlugin")' but required in type '{ doThing(): void; }'.
!!! related TS2728 main.ts:15:44: 'doThing' is declared here.
requirePlugin<{ doThing(): void }>("./notAPlugin"); // error
~~~~~~~~~~~~~~
!!! error TS2741: Property 'doThing' is missing in type 'typeof import("notAPlugin")' but required in type '{ doThing(): void; }'.
!!! related TS2728 main.ts:18:17: 'doThing' is declared here.

// A module reference is a string, and forwards to another module reference parameter.
declare function importDeferred<T>(ref: ModuleReference<T>): Promise<T>;
function forward<T>(ref: ModuleReference<T>): Promise<T> {
const specifier: string = ref;
specifier.length;
return importDeferred(ref);
}

importDeferred("./nope"); // error, no such module
~~~~~~~~
!!! error TS2307: Cannot find module './nope' or its corresponding type declarations.
importDeferred(String(1)); // error, not a string literal
~~~~~~~~~
!!! error TS2345: Argument of type 'string' is not assignable to parameter of type 'ModuleReference<unknown>'.

Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//// [tests/cases/conformance/types/typeAliases/moduleReferenceIntrinsic.ts] ////

//// [mod.ts]
export const getRandom = () => 10;
export const label = "mod";

//// [plugin.ts]
export function doThing(): void {}

//// [notAPlugin.ts]
export function somethingElse(): void {}

//// [main.ts]
declare const jest: {
requireActual<T>(ref: ModuleReference<T>): T;
mock<T>(ref: ModuleReference<T>, factory: () => NoInfer<T>): void;
};

const actual = jest.requireActual("./mod");
actual.getRandom();
actual.label;
actual.missing; // error, no such export

jest.mock("./mod", () => ({ ...jest.requireActual("./mod"), getRandom: () => 10 }));
jest.mock("./mod", () => ({ getRandom: () => 10 })); // error, 'label' is missing

// The referenced module is checked against the type parameter's constraint.
declare function requirePlugin<T extends { doThing(): void }>(ref: ModuleReference<T>): Promise<T>;
requirePlugin("./plugin");
requirePlugin("./notAPlugin"); // error
requirePlugin<{ doThing(): void }>("./notAPlugin"); // error

// A module reference is a string, and forwards to another module reference parameter.
declare function importDeferred<T>(ref: ModuleReference<T>): Promise<T>;
function forward<T>(ref: ModuleReference<T>): Promise<T> {
const specifier: string = ref;
specifier.length;
return importDeferred(ref);
}

importDeferred("./nope"); // error, no such module
importDeferred(String(1)); // error, not a string literal


//// [mod.js]
export const getRandom = () => 10;
export const label = "mod";
//// [plugin.js]
export function doThing() { }
//// [notAPlugin.js]
export function somethingElse() { }
//// [main.js]
"use strict";
const actual = jest.requireActual("./mod");
actual.getRandom();
actual.label;
actual.missing; // error, no such export
jest.mock("./mod", () => ({ ...jest.requireActual("./mod"), getRandom: () => 10 }));
jest.mock("./mod", () => ({ getRandom: () => 10 })); // error, 'label' is missing
requirePlugin("./plugin");
requirePlugin("./notAPlugin"); // error
requirePlugin("./notAPlugin"); // error
function forward(ref) {
const specifier = ref;
specifier.length;
return importDeferred(ref);
}
importDeferred("./nope"); // error, no such module
importDeferred(String(1)); // error, not a string literal


//// [mod.d.ts]
export declare const getRandom: () => number;
export declare const label = "mod";
//// [plugin.d.ts]
export declare function doThing(): void;
//// [notAPlugin.d.ts]
export declare function somethingElse(): void;
//// [main.d.ts]
declare const jest: {
requireActual<T>(ref: ModuleReference<T>): T;
mock<T>(ref: ModuleReference<T>, factory: () => NoInfer<T>): void;
};
declare const actual: typeof import("./mod");
declare function requirePlugin<T extends {
doThing(): void;
}>(ref: ModuleReference<T>): Promise<T>;
declare function importDeferred<T>(ref: ModuleReference<T>): Promise<T>;
declare function forward<T>(ref: ModuleReference<T>): Promise<T>;
Loading