-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Rule proposal: Export all types used in exports #7670
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
What about this: type User = { ... };
type Time = { ... };
export declare function query(str: string): { user: User; time: Time } {} Do we report anything? |
I'd think so, yes. Otherwise it'd be inconvenient to provide a type annotation on this import { query } from "...";
let queryResult /* : ...? */ ;
// ...
queryResult = query("..."); |
In this case even if we export those types the user still types Also there's the following pattern to consider: export namespace Foo {
export type T = ...;
export type U = ...;
}
export function foo(t: Foo.T, u: Foo.T): void; |
They can still use function withUserObject(user: User) {
// ...
}
withUserObject(queryResult.user); As long as the name of the type/etc. is accessible publicly, that'd satisfy my understanding of the rule. So that |
Every type reachable from the module's signature must be exported. There's no reason someone couldn't use Otherwise you have to do BS like thisimport * as webpack from 'webpack';
declare module 'webpack' {
import { Compilation, LoaderDefinitionFunction, Template } from 'webpack';
export type SourceMap = Exclude<NonNullable<Parameters<LoaderDefinitionFunction>[1]>, string>;
export type AdditionalData = NonNullable<Parameters<LoaderDefinitionFunction>[2]>;
export type LoaderContextResolveFn = ThisParameterType<LoaderDefinitionFunction>['resolve'];
export type LoaderContextGetResolveFn = ThisParameterType<LoaderDefinitionFunction>['getResolve'];
export type LoaderContextLoadModuleFn = ThisParameterType<LoaderDefinitionFunction>['loadModule'];
export type LoaderContextEmitErrorFn = ThisParameterType<LoaderDefinitionFunction>['emitError'];
export type LoaderContextEmitWarningFn =
ThisParameterType<LoaderDefinitionFunction>['emitWarning'];
export type ResolveOptionsWithDependencyType =
NonNullable<Parameters<LoaderContextGetResolveFn>[0]>;
export type ChunkGroup = ReturnType<Compilation['addChunkInGroup']>;
export type ChunkRenderContext = Parameters<typeof Template['renderChunkModules']>[1];
export type DependencyTemplate =
NonNullable<ReturnType<Compilation['dependencyTemplates']['get']>>;
export type DependencyTemplateContext = DependencyTemplate extends {
apply(dep: any, source: any, context: infer X): any,
} ? X
: never;
export type RenderManifestOptions = Parameters<Compilation['getRenderManifest']>[0];
} Yes - this is actual code we have at canva so that we can just write plain old webpack code with types. Not even delving into internals - just using their APIs. It's either that or you go "fuck it lets just patch the package". |
Bringing discussion back over from #8443 (comment):
Summarizing: I think the gray areas are when a type is used in an inline position. Particularly tricky when some type
I'll call those "wrappers": e.g. in Referring to what Brad said in #7670 (comment): every type reachable from the module's signature must be exported. We could start by defining "type" as a local identifier (e.g. But, they all entail varying level of difficulty to be reconstructed. For example, simple arrays ( The cleanest starting answer I can think of would be to ignore anything that's not a local identifier or object literal. Those are the main things that motivated this issue in the first place. So, going back to the
That's a little less strict from my suggestion in #8443 (comment). We could see how this works in userland for a while and maybe later add options to enable more strictness. IMO if we try to force people to extract types for simpler things like arrays and unions then they'll resist using the rule at all.
Oh, blurgh, this is a good question. Similar to // type-utilities.d.ts
export declare function identity<T>(value: T): T; // index.ts
import { identity } from "./type-utilities";
// You'd think the rule should error on this, right?
// ...but in pure AST land without types, that's not doable.
interface Box {
value: string;
}
// Implicit inferred return type: Box
function makeBox() {
return identity<Box>({ value: "abc" });
} The problem is, even with full type checking we wouldn't catch every case. Some constructs, namely literals and type Value = "abc";
// Implicit inferred return type: "abc"
// The Value type is interned in the type system
function makeBox() {
return identity<Value>("abc");
} My vote would be to not use type-checking at all, and comment is as an explicit out-of-scope point in the rule's docs. Anyway, pinging @StyleShit + @typescript-eslint/triage-team - what does everyone think? |
You put "tuple" in both "allow" and "report"—I assume you would allow What about What about enums in general? I assume while you allow |
Whoops, sorry, typo from too many re-edits of that post heh. I added it back to "allow".
My proposal is to allow those...
...so long as |
For |
Yes to both |
Great explanation of the issue! ✨
Well... "difficulty" is subjective, so I'm not sure how we can agree on a list of "easy" types to construct
Is this allowed? function fn<T extends string>(
arg: T[],
): [
(subscriber: (newValue: A, oldValue: A) => void) => void,
(
| (Record<string, B> & {
__brand: 'custom-brand';
})
| (Record<keyof C & keyof D, [T, T]> & {
__brand: 'another-brand';
})
),
];
Just to make sure - we'll allow
So things like |
Based on what my post is suggesting:
👍 that's what I'm suggesting. My suggestion is that the
👍 that's what I'm suggesting. My suggestion is that the |
IDK... even after stripping out the brand, this type is very complicated IMO. How would I construct it myself as a library consumer?
What's the difference in the "difficulty level" between Is My point is - it's very hard (impossible?) to tell when something is considered "difficult to construct" because it's subjective. I don't think we can cover all cases by arbitrarily saying "a tuple is ok, an object is not", because again, look at the 2 types above - do you believe that constructing the tuple is easier than constructing the object? Maybe we can consider things like:
Yes, it's odd, but I think it makes more sense in this case when looking from the user's POV (it's probably gonna be a nightmare to develop, and won't catch all cases) Maybe we can make it configurable and have some reasonable defaults? 🤷 Anyway, I think we need to find that sweet spot between "not making the library developers hate us and turn off the rule" and "helping the library consumers by making sure library developers will export their types" 😅 |
Yeah I think that at an MVP level enforcing "all exported things use named types" is a good standard and is easy to lint for. Depends how deep we want to go before we've even written anything tbh. |
Yeah. @StyleShit you bring up a lot of good questions on rule nuances, but for an initial version of this rule, we don't want to have so much nuance encoded in it. Arbitrary delineations like "only X up to Y items" tend to confuse end-users and feel unnecessarily arbitrary. In large part because they are.
For the initial version, let's do the same strategy as
This isn't going to be perfect in a v1, and that's ok. The best thing we can do now is start it as straightforward & understandable as possible. |
@bradzacher, just to make sure I understand you correctly, are you proposing a different approach than @JoshuaKGoldberg? It sounds to me more like my proposal in #8443 (comment):
Am I missing something? |
No yeah Josh's comment was fine. We could spin our wheels and chat about the details for ages but really it depends on how it feels and how users react. We may find that people are happy with the dumb rule and don't care about it being strict. But we will likely find people want relaxations. But who knows! Release and iterate let's go |
So to make sure we're all aligned: We wanna go with an initial version that forces you to have an exported type alias for any parameter/return-type that's an inline object literal, while still allowing any arrays, tuples, etc. that "wrap" a type alias? In addition, what about imported types that are being used inside an exported function? Should we force re-exporting them? |
👍 from me.
Let's not require re-exports. Knowing whether a type is or isn't exported across a system in some way is outside the scope of a lint rule. That'd be more in the realm of a true project-wide checker such as Knip. (not saying it should go in Knip, just that we operate at different scales) |
Sounds good to me. Will try to find some time to work on it 😄 |
Before You File a Proposal Please Confirm You Have Done The Following...
My proposal is suitable for this project
Description
If a file exports a construct that relies on some other types, but doesn't export those other types, it can be difficult for other files to declare types based on the construct.
For example, suppose a
./somewhere
file exports adoSomething
function with a single parameter of typeSomeSettings
, but doesn't exportSomeSettings
. What if a consuming file wants to declare a local/parameter of that sameSomeSettings
type? There'd be no easy way to refer toSomeSettings
.I see this a lot in definitions for libraries. Example: saurabhnemade/react-twitter-embed#132
Fail Cases
Pass Cases
Additional Info
...I could have sworn this was already filed somewhere, but I couldn't find it after a few minutes of searching. 🤷
The text was updated successfully, but these errors were encountered: