Skip to content
Merged
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

This file was deleted.

24 changes: 23 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions packages/jsts/src/rules/S4335/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export const rule: Rule.RuleModule = {
if (isLiteralUnionPattern(intersection)) {
continue;
}
if (isGenericTypePattern(intersection, typeNode, parserServices)) {
continue;
}
context.report({
messageId: 'removeIntersection',
node: typeNode as unknown as estree.Node,
Expand Down Expand Up @@ -158,3 +161,43 @@ function isLiteralUnionPattern(intersection: TSESTree.TSIntersectionType): boole
otherType.type === 'TSTypeReference'
);
}

/**
* Detects generic type patterns where `& {}` serves a legitimate purpose:
* 1. Mapped type pattern: `{ [K in keyof T]: T[K] } & {}` - forces type flattening (Simplify/Prettify)
* 2. Generic type reference: `GenericType<T> & {}` where GenericType has type arguments
* 3. Type parameter reference: `T & {}` where T is a type parameter
*
* These patterns are used for type normalization and non-nullability constraints.
*/
function isGenericTypePattern(
intersection: TSESTree.TSIntersectionType,
emptyTypeNode: TSESTree.TypeNode,
parserServices: RequiredParserServices,
): boolean {
const siblingTypes = intersection.types.filter(t => t !== emptyTypeNode);

return siblingTypes.some(siblingType => {
// Pattern 1: Mapped type (Simplify/Prettify pattern)
if (siblingType.type === 'TSMappedType') {
return true;
}

// Pattern 2: Generic type reference with type arguments
if (siblingType.type === 'TSTypeReference' && siblingType.typeArguments) {
return true;
}

// Pattern 3: Type parameter reference (e.g., T & {})
if (siblingType.type === 'TSTypeReference') {
const tp: ts.Type = parserServices.program
.getTypeChecker()
.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(siblingType));
if (tp.isTypeParameter()) {
return true;
}
}

return false;
});
}
78 changes: 70 additions & 8 deletions packages/jsts/src/rules/S4335/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,76 @@ interface HttpResourceRequestOptions {
cache?: RequestCache | (string & {});
credentials?: RequestCredentials | (string & {});
priority?: RequestPriority | (string & {});
}`,
},
// False positive tests: Generic type manipulation patterns with & {}
{
// Simplify/Prettify mapped type pattern: { [K in keyof T]: T[K] } & {}
// The & {} forces TypeScript to flatten/simplify complex type representations.
// This is a well-known TypeScript idiom used in libraries like Kibana.
// See: https://github.com/microsoft/TypeScript/issues/29729
code: `
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};

interface ComplexType {
prop1: string;
nested: {
value: number;
};
}

type Simplified = Simplify<ComplexType>;`,
},
{
// Generic type reference with type arguments intersected with {}
// Used for type normalization in complex generic type expressions.
// Real-world example pattern from TanStack Router.
code: `
interface SomeProps<T> { value: T; }
type ExtendedProps<T> = SomeProps<T> & {};`,
},
{
// Generic type reference in interface property position
// Pattern from TanStack Router: params: ParamsReducer<TRouter, TFrom, TTo> & {}
code: `
interface AnyRouter {
routes: unknown;
}

type ParamsReducer<TRouter extends AnyRouter, TFrom, TTo> = {
router: TRouter;
from: TFrom;
to: TTo;
};

interface MakeRequiredPathParams<TRouter extends AnyRouter, TFrom, TTo> {
params: ParamsReducer<TRouter, TFrom, TTo> & {};
}`,
},
{
// Reversed order: {} & GenericType pattern
// Used for non-nullability constraints in generic type definitions.
// Real-world example from apollo-client DeepPartial.ts.
code: `
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;

type DeepPartialSet<T> = {} & Set<DeepPartial<T>>;
type DeepPartialReadonlySet<T> = {} & ReadonlySet<DeepPartial<T>>;
type DeepPartialMap<K, V> = {} & Map<DeepPartial<K>, DeepPartial<V>>;`,
},
{
// Bare type parameter intersected with {}
// T & {} filters out null/undefined from T, a common generic constraint pattern.
code: `type NonNullish<T> = T & {};`,
},
{
// Generic type reference with & {} inside a function body
// Real-world pattern from ant-design: type alias using enclosing function's type parameter
code: `
interface PickerProps<T> { value: T; }
function generatePicker<DateType>() {
type InternalProps = PickerProps<DateType> & {};
return {} as InternalProps;
}`,
},
],
Expand Down Expand Up @@ -204,14 +274,6 @@ interface HttpResourceRequestOptions {
code: `type AlsoInvalid = number & {};`,
errors: 1,
},
{
// Type reference intersected with {} outside of union should be flagged
// This is genuinely redundant (unlike the LiteralUnion pattern in unions)
code: `
interface SomeProps<T> { value: T; }
type ExtendedProps<T> = SomeProps<T> & {};`,
errors: 1,
},
],
},
);
Expand Down
Loading