From 028628bc241ead4a3ab4c58c18a29774b5cd15a9 Mon Sep 17 00:00:00 2001 From: uhyo Date: Wed, 1 May 2024 06:37:21 +0900 Subject: [PATCH] fix: improve types for .attrs() (#4288) * fix: improve attrs() typing * fix: update constructWithOptions type * fix: allow double overriding * test: add double attrs test * fix: support double attrs pattern * chore: remove unused generic after refactoring --------- Co-authored-by: Evan Jacobs --- .../src/constructors/constructWithOptions.ts | 119 +++++++++++------- packages/styled-components/src/test/types.tsx | 64 ++++++++++ 2 files changed, 137 insertions(+), 46 deletions(-) diff --git a/packages/styled-components/src/constructors/constructWithOptions.ts b/packages/styled-components/src/constructors/constructWithOptions.ts index e7f74fde7..c3092fc9a 100644 --- a/packages/styled-components/src/constructors/constructWithOptions.ts +++ b/packages/styled-components/src/constructors/constructWithOptions.ts @@ -1,7 +1,6 @@ import { Attrs, BaseObject, - ExecutionProps, Interpolation, IStyledComponent, IStyledComponentFactory, @@ -26,30 +25,25 @@ type AttrsResult> = T extends (...args: any) => infer P : never; /** - * Based on Attrs being a simple object or function that returns - * a prop object, inspect the attrs result and attempt to extract - * any "as" prop usage to modify the runtime target. + * Extract non-optional fields from given object type. */ -type AttrsTarget< - R extends Runtime, - T extends Attrs, - FallbackTarget extends StyledTarget, - Result extends ExecutionProps = AttrsResult, -> = Result extends { as: infer RuntimeTarget } - ? RuntimeTarget extends KnownTarget - ? RuntimeTarget - : FallbackTarget - : FallbackTarget; +type RequiredFields = Pick< + T, + { + [K in keyof T]-?: undefined extends T[K] ? never : K; + }[Exclude] +>; export interface Styled< R extends Runtime, Target extends StyledTarget, OuterProps extends object, OuterStatics extends object = BaseObject, + InnerProps extends object = OuterProps, > { ( - initialStyles: Styles>>, - ...interpolations: Interpolation>>[] + initialStyles: Styles>>, + ...interpolations: Interpolation>>[] ): IStyledComponent> & OuterStatics & Statics & @@ -63,24 +57,58 @@ export interface Styled< Props extends object = BaseObject, PrivateMergedProps extends object = Substitute, PrivateAttrsArg extends Attrs = Attrs, - PrivateResolvedTarget extends StyledTarget = AttrsTarget, >( attrs: PrivateAttrsArg - ) => Styled< + ) => StyledAttrsResult< R, - PrivateResolvedTarget, - PrivateResolvedTarget extends KnownTarget - ? Substitute< - Substitute>, - Props - > - : PrivateMergedProps, - OuterStatics + Target, + OuterProps, + OuterStatics, + InnerProps, + Props, + PrivateMergedProps, + PrivateAttrsArg >; withConfig: (config: StyledOptions) => Styled; } +type StyledAttrsResult< + R extends Runtime, + Target extends StyledTarget, + OuterProps extends object, + OuterStatics extends object = BaseObject, + InnerProps extends object = OuterProps, + Props extends object = BaseObject, + PrivateMergedProps extends object = Substitute, + PrivateAttrsArg extends Attrs = Attrs, +> = ( + AttrsResult extends { as: infer RuntimeTarget extends KnownTarget } + ? { + Target: RuntimeTarget; + TargetProps: Substitute>; + } + : { Target: Target; TargetProps: OuterProps } +) extends { + Target: infer PrivateResolvedTarget extends StyledTarget; + TargetProps: infer TargetProps extends object; +} + ? Styled< + R, + PrivateResolvedTarget, + PrivateResolvedTarget extends KnownTarget + ? Substitute>> + : PrivateMergedProps, + OuterStatics, + PrivateResolvedTarget extends KnownTarget + ? Substitute< + Substitute>, + Props + > + : PrivateMergedProps + > + : unknown; + export default function constructWithOptions< R extends Runtime, Target extends StyledTarget, @@ -88,11 +116,12 @@ export default function constructWithOptions< ? React.ComponentPropsWithRef : BaseObject, OuterStatics extends object = BaseObject, + InnerProps extends object = OuterProps, >( componentConstructor: IStyledComponentFactory, object, any>, tag: StyledTarget, options: StyledOptions = EMPTY_OBJECT -): Styled { +): Styled { /** * We trust that the tag is a valid component as long as it isn't * falsish. Typically the tag here is a string or function (i.e. @@ -106,13 +135,13 @@ export default function constructWithOptions< /* This is callable directly as a template function */ const templateFunction = ( - initialStyles: Styles>, - ...interpolations: Interpolation>[] + initialStyles: Styles>, + ...interpolations: Interpolation>[] ) => - componentConstructor, Statics>( + componentConstructor, Statics>( tag, - options as StyledOptions>, - css>(initialStyles, ...interpolations) + options as StyledOptions>, + css>(initialStyles, ...interpolations) ); /** @@ -125,24 +154,22 @@ export default function constructWithOptions< Props extends object = BaseObject, PrivateMergedProps extends object = Substitute, PrivateAttrsArg extends Attrs = Attrs, - PrivateResolvedTarget extends StyledTarget = AttrsTarget, >( attrs: PrivateAttrsArg - ) => - constructWithOptions< - R, - PrivateResolvedTarget, - PrivateResolvedTarget extends KnownTarget - ? Substitute< - Substitute>, - Props - > - : PrivateMergedProps, - OuterStatics - >(componentConstructor, tag, { + ): StyledAttrsResult< + R, + Target, + OuterProps, + OuterStatics, + InnerProps, + Props, + PrivateMergedProps, + PrivateAttrsArg + > => + constructWithOptions(componentConstructor, tag, { ...options, attrs: Array.prototype.concat(options.attrs, attrs).filter(Boolean), - }); + }) as any; /** * If config methods are called, wrap up a new template function diff --git a/packages/styled-components/src/test/types.tsx b/packages/styled-components/src/test/types.tsx index a9fd46cc3..28c98bfc2 100644 --- a/packages/styled-components/src/test/types.tsx +++ b/packages/styled-components/src/test/types.tsx @@ -270,6 +270,70 @@ const AttrRequiredTest4 = styled(DivWithUnfulfilledRequiredProps).attrs({ waz: 42, })``; +{ + const DivWithRequiredFooBar = styled.div<{ foo: number; bar: string }>``; + // @ts-expect-error must provide both foo and bar + ; + // @ts-expect-error must provide both foo and bar + ; + // @ts-expect-error must provide both foo and bar + ; + // OK + ; + + // foo is provided, so it becomes optional + const DivWithRequiredBar = styled(DivWithRequiredFooBar).attrs({ foo: 42 })` + margin; ${props => props.foo * 10}px; + `; + // @ts-expect-error must provide bar + ; + // OK + ; + // OK. Can still provide foo if we want + ; + // @ts-expect-error foo must be a number + ; + + const Div = styled(DivWithRequiredBar).attrs({ bar: '42' })` + margin: ${props => { + // @ts-expect-error foo is optional + const foo: number = props.foo; + const bar: string = props.bar; + return foo * Number(bar); + }}px; + `; + // OK +
; +
; +
; +
; +} + +{ + // double attrs + const DivWithRequiredFooBar = styled.div<{ foo: number; bar: number }>``; + const Div = styled(DivWithRequiredFooBar).attrs({ foo: 42 }).attrs({ bar: 42 })` + margin: ${props => props.foo * props.bar}px; + `; + +
; +
; +
; +
; +} + +{ + // double overriding + const Div = styled.div``; + const H1 = styled(Div).attrs({ as: 'h1' })``; + const Label = styled(H1).attrs({ as: 'label' })``; + +