Skip to content

Latest commit

Β 

History

History
429 lines (308 loc) Β· 12.1 KB

no-type-export-outside-types.md

File metadata and controls

429 lines (308 loc) Β· 12.1 KB

e2ed/no-type-export-outside-types

πŸ“ Disallow type exports outside the autotests/types directory.

πŸ’Ό This rule is enabled in the following configs: 🌐 all, βœ… recommended.

In e2ed projects all reusable (and potentially reusable) types live in the autotests/types directory, and this directory contains only types. Together with the complementary no-value-export-in-types rule (which forbids value exports in the types directories) this rule keeps the invariant: a type that is used in more than one file is declared in autotests/types and is exported only from there. Any other file of the autotests exports only runtime values and imports the types it needs from autotests/types.

The rule applies to all files inside the autotests directory (at any depth) except the files in autotests/types and in the directories listed in the additionalTypesDirectories option (autotests/configurator/types by default). In such files every top-level export must be a value export: an export with the type modifier (export type Foo = ..., export interface Foo {}, export type {Foo}, export type * from '...'), an inline type modifier on a specifier (export {type Foo, bar}), an ambient export declare ... and export default interface Foo {} are reported.

Examples

❌ Incorrect

// autotests/pageObjects/SearchPage.ts

export type ProductCardState = 'available' | 'soldOut';

// Type re-exports
export type {Product} from './Product';

export type {Product as SearchProduct, ProductId} from './Product';

export type * from './Product';

export type * as Product from './Product';

// Every type specifier is reported separately
export {type Product as SearchProduct, type ProductId, searchPage} from './Product';

// Default exports of interfaces
export default interface SearchFilters {}

// Ambient declarations exist only at the type level and belong to `autotests/types` as well
export declare const searchLimit: number;

export declare function getProductId(product: Product): ProductId;

export declare class SearchQuery {}

// An empty `export type {}` is reported as well: use `export {}`
export type {};
// autotests/pageObjects/SearchPage.ts

export interface SearchFilters {
  minPrice: number;
}
// autotests/pageObjects/SearchPage.ts

// Re-exporting a local type
type ProductId = string;

export type {ProductId};
// autotests/pageObjects/SearchPage.ts

// Only the specifier with the inline `type` modifier is reported
type ProductId = string;
const searchLimit = 10;

export {type ProductId, searchLimit};
// autotests/actions/index.ts

export {getRequestCounter, type RequestCounter} from './getRequestCounter';
// autotests/pageObjects/SearchPage.ts

export declare enum Language {
  PT_BR = 'pt-BR',
}
// autotests/pageObjects/SearchPage.ts

export declare namespace Search {
  const limit: number;
}
// autotests/pageObjects/SearchPage.ts

// A type export next to a value export is still reported
export type Language = 'pt' | 'en';

export const defaultLanguage: Language = 'pt';
// autotests/entities/deal/createFinancingDealWithBoleto.ts

export type DealWithBoleto = Deal & Readonly<{boletoUrl: string}>;
// autotests/tests/search/searchPage.ts

// Test files are checked as well
export type SearchTestContext = Readonly<{query: string}>;
// autotests/pageObjects/types/search.ts

// A `types` directory outside `autotests/types` is checked
export type ProductCardState = 'available' | 'soldOut';
// autotests/types-old/search.ts

// The directory must be named exactly `types`
export type ProductCardState = 'available' | 'soldOut';
// autotests/configurator/index.ts

// By default only `autotests/configurator/types` is allowed, not the whole `configurator` directory
export type {Pack} from './types';
// autotests/configurator/types/pack.ts

// With `additionalTypesDirectories: []` the `configurator` types are reported too
export type Pack = Readonly<{
  testIdleTimeout: number;
}>;
// autotests/configurator/types/pack.ts

// With `additionalTypesDirectories: ["autotests/hooks"]` the default directory is not allowed anymore: the option replaces the default list
export type {Pack} from './pack';

βœ… Correct

// autotests/pageObjects/SearchPage.ts

// Reusable types are imported from `autotests/types` and only values are exported
import type {Product, ProductId} from 'autotests/types';

export const getProductId = (product: Product): ProductId => product.id;
// autotests/pageObjects/SearchPage.ts

export class SearchPage extends BasePage {}

export const searchLimit = 10;

// Value re-exports
export {searchLimit} from 'autotests/constants';

export * from './SearchFilters';

export * as searchFilters from './SearchFilters';

// Default exports of values
export default class SearchPage {}

// An empty `export {}` is a value export
export {};

// An `export import` alias is not distinguishable from a value export and is not checked
export import ProductId = Entities.ProductId;
// autotests/pageObjects/SearchPage.ts

export function getProductId(product: Product): ProductId {
  return product.id;
}
// autotests/pageObjects/SearchPage.ts

// `enum` is a runtime value, not a type
export enum Language {
  PT_BR = 'pt-BR',
}
// autotests/pageObjects/SearchPage.ts

export const enum Currency {
  BRL = 'BRL',
}
// autotests/pageObjects/SearchPage.ts

// Type exports inside a `namespace` block are not exports of the file
export namespace Search {
  export type Query = string;
}
// autotests/pageObjects/SearchPage.ts

const searchLimit = 10;

export {searchLimit};
// autotests/pageObjects/SearchPage.ts

// A type used in one file only stays local, without `export`
type SearchQuery = string;

export const search = (query: SearchQuery): Promise<void> => searchPage.search(query);
// autotests/pageObjects/SearchPage.ts

// Exports inside `declare module` are not checked
declare module 'autotests/constants' {
  export type SearchLimit = number;
}
// autotests/pageObjects/SearchPage.ts

// Exports inside `declare global` are not checked
declare global {
  export interface Window {
    searchPage: SearchPage;
  }
}
// autotests/pageObjects/SearchPage.ts

// A default export of a type alias looks like a value export and is not checked
type ProductId = string;

export default ProductId;
// autotests/types/product.ts

// Type exports are allowed in the `autotests/types` directory
export type ProductId = Brand<string, 'ProductId'>;

export type {Product} from './product';

export type * from './entities';
// autotests/types/entities/product.ts

// At any depth inside `autotests/types`
export interface Product {
  id: ProductId;
}
// autotests/configurator/types/pack.ts

// Type exports are allowed in the directories from the `additionalTypesDirectories` option (`autotests/configurator/types` by default)
export type Pack = Readonly<{
  testIdleTimeout: number;
}>;
// autotests/configurator/types/index.ts

export type {Pack} from './pack';
// autotests/configurator/types/qase/run.ts

// At any depth inside an allowed directory
export type RunUpdate = Readonly<{title: string}>;
// autotests/configurator/index.ts

// With `additionalTypesDirectories: ["autotests/configurator"]` the whole `configurator` directory is allowed
export type {Pack} from './types';
// autotests/hooks/context.ts

// With `additionalTypesDirectories: ["autotests/configurator/types", "autotests/hooks"]` several directories are allowed
export type HooksContext = Readonly<{startedAt: number}>;
// src/types/product.ts

// Files outside the `autotests` directory are not checked
export type ProductId = Brand<string, 'ProductId'>;
// my-autotests/pageObjects/SearchPage.ts

// The directory must be named exactly `autotests`
export type ProductId = Brand<string, 'ProductId'>;

Options

The rule accepts an object with a single property:

  • additionalTypesDirectories (string[], default: ["autotests/configurator/types"]) β€” types directories in addition to autotests/types: directories that contain only types, so type exports are allowed there. autotests/types itself is always a types directory and cannot be changed or removed, so that every e2ed project has the same structure. Each entry is a normalized path (with / as the separator and without a trailing slash) that is matched as a sequence of whole path segments anywhere in the file path, so autotests/configurator/types matches /repo/autotests/configurator/types/pack.ts but not autotests/configurator/types-old/pack.ts. The option replaces the default list instead of extending it: include autotests/configurator/types explicitly if it should stay allowed, or pass an empty array to allow type exports in autotests/types only.
{
  rules: {
    'e2ed/no-type-export-outside-types': [
      'error',
      {additionalTypesDirectories: ['autotests/configurator/types', 'autotests/hooks']},
    ],
  },
}

The option has the same name, the same default and the same meaning as the option of no-value-export-in-types: the types directories are exactly the directories where type exports are allowed and value exports are forbidden. Keep the two options equal, so that every listed directory contains only types and the types are imported only from there.

The autotests/configurator directory is allowed by default because of the e2ed contract: e2ed itself imports the pack types (Pack, TestMeta and others) from autotests/configurator, and modules of the configurator cannot import anything from other autotests/* directories, including autotests/types, since the pack config is evaluated before all other code. Keep such types in autotests/configurator/types and re-export them from autotests/configurator/index.ts with export type {...} from './types'.

Rule details

The rule is purely syntactic: it checks the presence of the type modifier and does not use type information. An export with the modifier is reported as a whole (export type {Product, ProductId} from './product' is one report); an inline modifier is reported per specifier, so in export {type Product, type ProductId, searchPage} two specifiers are reported and the value specifier is not. An empty export type {} is reported as well β€” write export {} to mark the file as a module.

Ambient declarations (export declare const ..., export declare namespace ...) exist only at the type level, so they are reported too: they belong to autotests/types. Exports inside namespace, declare module and declare global blocks are not exports of the file and are not checked.

Two forms are not distinguishable from value exports without type information and are not reported: a default export of a type alias (type ProductId = string; export default ProductId;) and an export import alias (export import ProductId = Entities.ProductId;). Neither is used in e2ed autotests. CommonJS exports and the TypeScript-specific export = ... are not checked either: e2ed autotests are written in ES modules.

A type that is used in one file only does not need to be exported at all: declare it without export next to the code that uses it.

When not to use it

The rule enforces a core convention of e2ed and should be enabled in every e2ed project. The only reason to keep it disabled is a legacy project that still declares shared types next to page objects, entities or actions and has not yet moved them to autotests/types.

Further reading