diff --git a/packages/styled-components/src/constructors/constructWithOptions.ts b/packages/styled-components/src/constructors/constructWithOptions.ts index f7a475992..d9e1641ff 100644 --- a/packages/styled-components/src/constructors/constructWithOptions.ts +++ b/packages/styled-components/src/constructors/constructWithOptions.ts @@ -1,6 +1,7 @@ import { Attrs, BaseObject, + ExecutionProps, Interpolation, IStyledComponent, IStyledComponentFactory, @@ -25,25 +26,30 @@ type AttrsResult> = T extends (...args: any) => infer P : never; /** - * Extract non-optional fields from given object type. + * 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. */ -type RequiredFields = Pick< - T, - { - [K in keyof T]-?: undefined extends T[K] ? never : K; - }[Exclude] ->; +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; 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 & @@ -57,58 +63,24 @@ export interface Styled< Props extends object = BaseObject, PrivateMergedProps extends object = Substitute, PrivateAttrsArg extends Attrs = Attrs, + PrivateResolvedTarget extends StyledTarget = AttrsTarget, >( attrs: PrivateAttrsArg - ) => StyledAttrsResult< + ) => Styled< R, - Target, - OuterProps, - OuterStatics, - InnerProps, - Props, - PrivateMergedProps, - PrivateAttrsArg + PrivateResolvedTarget, + PrivateResolvedTarget extends KnownTarget + ? Substitute< + Substitute>, + Props + > + : PrivateMergedProps, + OuterStatics >; 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, @@ -116,12 +88,11 @@ 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. @@ -135,13 +106,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) ); /** @@ -154,22 +125,24 @@ export default function constructWithOptions< Props extends object = BaseObject, PrivateMergedProps extends object = Substitute, PrivateAttrsArg extends Attrs = Attrs, + PrivateResolvedTarget extends StyledTarget = AttrsTarget, >( attrs: PrivateAttrsArg - ): StyledAttrsResult< - R, - Target, - OuterProps, - OuterStatics, - InnerProps, - Props, - PrivateMergedProps, - PrivateAttrsArg - > => - constructWithOptions(componentConstructor, tag, { + ) => + constructWithOptions< + R, + PrivateResolvedTarget, + PrivateResolvedTarget extends KnownTarget + ? Substitute< + Substitute>, + Props + > + : PrivateMergedProps, + OuterStatics + >(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 f4e8825fe..2e9d2b994 100644 --- a/packages/styled-components/src/test/types.tsx +++ b/packages/styled-components/src/test/types.tsx @@ -270,70 +270,6 @@ 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' })``; - -