Suggestion
π Search Terms
module reference intrinsic type
β
Viability Checklist
My suggestion meets these guidelines:
Background
In the ecmascript ecosystem it's not uncommon to have tools that accept one or more paths to TS module files. For example when working with jest mocks you might define a mock to patch some behaviour and require the non-mocked module to maintain the rest of the module:
jest.mock('../my-module', () => {
const originalModule = jest.requireActual('../my-module');
return {
...originalModule,
getRandom: jest.fn(() => 10),
};
});
There is also usecases outside outside of tests for custom wrapper APIs to do manage importing of files. For example one could create an API like importDeferred which causes a bundler to bundle the dependency in a specific way.
The unfortunate problem with these sorts of APIs in TS is that there's no way to statically type the input or output - the module paths are typed as string and the return values are typed as any/unknown, or even worse they leverage the "type parameter in a return location only" antipattern (function importDeferred<T>(string): T -> importDeferred<{ trustMe: 'lol' }>('./module')).
Instead these patterns just rely upon bundle-time or runtime validation to catch mistakes - which is a frustrating break in the workflow.
There are some difficulties that exist in ensuring return types are also correct. For example in the above jest mock case - if we were to refactor ../my-module so that it no longer exports a getRandom function - then our test will not fail, and our types will validate fine - the only way to get around this currently is to manually import the module type and then to explicitly type the return:
import type * as MyModuleType from '../my-module';
jest.mock('../my-module', (): MyModuleType => { /* ... */ });
Which works, but is a bit cumbersome.
Proposal
I propose that TypeScript add the following intrinsic type: type ModuleReference<T> = intrinsic;
This intrinsic type would accept string literals (or variables typed as ModuleReference<T>) and those string literals would be validated by the type system to be a valid path using the exact same logic used to validate import statements/expressions.
The generic type parameter specifies the expected module shape based on the resolution of the path. For example the import expression could be defined as follows:
declare function import<T>(ref: ModuleReference<T>): Promise<T>;
Use Cases
Jest
And the jest APIs could be defined as follows:
declare const jest: {
requireActual: <T>(ref: ModuleReference<T>) => T;
mock: <T>(ref: ModuleReference<T>, mockCb: () => T): void;
};
APIs that expect explicit module shapes
One could also define an API that expects a certain shape for the module:
async function requirePlugin<T extends { doThing: () => void }>(
ref: ModuleReference<T>,
): void {
const plugin = await import(ref);
plugin.doThing();
}
Module APIs
It also opens the door for augmenting the module:
async function augmentModule<T extends object>(
ref: ModuleReference<T>,
): Promise<T & { augmentation: string }> {
const mod = await import(ref);
return {
...mod,
augmentation: 'woo hoo!',
};
}
Feature Gating / Conditional Imports
At several companies I've also seen experimentation / feature gating implemented in tooling with APIs that look like this:
const module = await importGated('feature_gate_name', {
pass: './module1',
fail: './module2',
});
Which could be defined like:
declare function importGated<T>(featureGateName: string, modules: {
pass: ModuleReference<T>,
fail: ModuleReference<NoInfer<T>>,
}): Promise<T>;
Or they have APIs that look like this:
const maybeModule = featureGate('feature_gate_name') ? await import('./module') : null;
Which could be improved with these types to
declare function conditionalImport<T>(
featureGateName: string,
ref: ModuleReference<T>,
): Promise<T> | null;
const maybeModule = await conditionalImport('feature_gate_name', './module');
A big win I see from this is that it would allow TS to type NodeJS's require function - which would be a big win for codebases that leverage require as an "inline synchronous import" (which is still a very common pattern).
Finally the TypeScript LSP could also recognise strings that are typed with ModuleRef and provide "go to definition" for these string literals, just like they would with import statements/expressions. This would be a big win for DevX.
Prior Art
Whilst it's not documented externally there is an existing system built into flow.
Flow declares the $Flow$ModuleRef type which pairs with the haste_module_ref_prefix config flag to allow you to do the same thing (specifically when using the haste module resolver).
For example internally at Meta the codebase declares various "require" functions that do different things in the build system and accept string literals that are checked by flow to be valid module references.
The haste_module_ref_prefix option defines a prefix that must exist on the string literals before flow will validate them. IIRC this exists because the build tools that predated flow are single-file only and the prefix allowed them explicitly pick out the module path strings and validate them.
For example in the Meta codebase haste_module_ref_prefix='m#' - so you see code like requireDeferred('m#MyModule').
It's up to the team as to whether such a system would be required - it does have the added bonus of making it trivial for tools to understand what string literals are module reference purely based on quick regex check as opposed to needing to be fully type-aware.
Suggestion
π Search Terms
module reference intrinsic type
β Viability Checklist
My suggestion meets these guidelines:
Background
In the ecmascript ecosystem it's not uncommon to have tools that accept one or more paths to TS module files. For example when working with jest mocks you might define a mock to patch some behaviour and require the non-mocked module to maintain the rest of the module:
There is also usecases outside outside of tests for custom wrapper APIs to do manage importing of files. For example one could create an API like
importDeferredwhich causes a bundler to bundle the dependency in a specific way.The unfortunate problem with these sorts of APIs in TS is that there's no way to statically type the input or output - the module paths are typed as
stringand the return values are typed asany/unknown, or even worse they leverage the "type parameter in a return location only" antipattern (function importDeferred<T>(string): T->importDeferred<{ trustMe: 'lol' }>('./module')).Instead these patterns just rely upon bundle-time or runtime validation to catch mistakes - which is a frustrating break in the workflow.
There are some difficulties that exist in ensuring return types are also correct. For example in the above jest mock case - if we were to refactor
../my-moduleso that it no longer exports agetRandomfunction - then our test will not fail, and our types will validate fine - the only way to get around this currently is to manually import the module type and then to explicitly type the return:Which works, but is a bit cumbersome.
Proposal
I propose that TypeScript add the following intrinsic type:
type ModuleReference<T> = intrinsic;This intrinsic type would accept string literals (or variables typed as
ModuleReference<T>) and those string literals would be validated by the type system to be a valid path using the exact same logic used to validateimportstatements/expressions.The generic type parameter specifies the expected module shape based on the resolution of the path. For example the
importexpression could be defined as follows:Use Cases
Jest
And the jest APIs could be defined as follows:
APIs that expect explicit module shapes
One could also define an API that expects a certain shape for the module:
Module APIs
It also opens the door for augmenting the module:
Feature Gating / Conditional Imports
At several companies I've also seen experimentation / feature gating implemented in tooling with APIs that look like this:
Which could be defined like:
Or they have APIs that look like this:
Which could be improved with these types to
A big win I see from this is that it would allow TS to type NodeJS's
requirefunction - which would be a big win for codebases that leveragerequireas an "inline synchronous import" (which is still a very common pattern).Finally the TypeScript LSP could also recognise strings that are typed with
ModuleRefand provide "go to definition" for these string literals, just like they would with import statements/expressions. This would be a big win for DevX.Prior Art
Whilst it's not documented externally there is an existing system built into flow.
Flow declares the
$Flow$ModuleReftype which pairs with thehaste_module_ref_prefixconfig flag to allow you to do the same thing (specifically when using the haste module resolver).For example internally at Meta the codebase declares various "require" functions that do different things in the build system and accept string literals that are checked by flow to be valid module references.
The
haste_module_ref_prefixoption defines a prefix that must exist on the string literals before flow will validate them. IIRC this exists because the build tools that predated flow are single-file only and the prefix allowed them explicitly pick out the module path strings and validate them.For example in the Meta codebase
haste_module_ref_prefix='m#'- so you see code likerequireDeferred('m#MyModule').It's up to the team as to whether such a system would be required - it does have the added bonus of making it trivial for tools to understand what string literals are module reference purely based on quick regex check as opposed to needing to be fully type-aware.