From 59ab5b3aef80fa55010f7d8385f785514d2e0b1a Mon Sep 17 00:00:00 2001 From: Jessica Date: Wed, 14 Nov 2018 13:14:32 +0900 Subject: [PATCH 1/6] Add more complete support for object styles, tests --- types/styled-components/index.d.ts | 163 ++++++++--------- types/styled-components/test/index.tsx | 237 +++++++++++++++++-------- 2 files changed, 249 insertions(+), 151 deletions(-) diff --git a/types/styled-components/index.d.ts b/types/styled-components/index.d.ts index 25d006f6f1..6398ee36d5 100644 --- a/types/styled-components/index.d.ts +++ b/types/styled-components/index.d.ts @@ -9,10 +9,21 @@ /// -import * as React from 'react'; -import * as CSS from 'csstype'; +import * as React from "react"; +import * as CSS from "csstype"; -export type CSSObject = CSS.Properties; +export type CSSObject = CSS.Properties & + // Index type to allow selector nesting + // This is "[key in string]" and not "[key: string]" to allow CSSObject to be self-referential + { + // we need the CSS.Properties in here too to ensure the index signature doesn't create impossible values + [key in string]: + | CSS.Properties[keyof CSS.Properties< + string | number + >] + | CSSObject + }; +export type CSSKeyframes = object & { [key: string]: CSSObject }; export interface ThemeProps { theme: T; @@ -45,27 +56,25 @@ type StyledComponentPropsWithAs< export type FalseyValue = undefined | null | false; export type Interpolation

= - | FlattenInterpolation

- | ReadonlyArray< - FlattenInterpolation

| ReadonlyArray> - >; -export type FlattenInterpolation

= | InterpolationValue - | InterpolationFunction

; + | InterpolationFunction

+ | FlattenInterpolation

; +// must be an interface to be self-referential +export interface FlattenInterpolation

+ extends ReadonlyArray> {} export type InterpolationValue = | string | number - | Styles | FalseyValue | Keyframes | StyledComponentInterpolation | CSSObject; export type SimpleInterpolation = | InterpolationValue - | ReadonlyArray>; -export interface Styles { - [ruleOrSelector: string]: string | number | Styles; -} + | FlattenSimpleInterpolation; +// must be an interface to be self-referential +interface FlattenSimpleInterpolation + extends ReadonlyArray {} export type InterpolationFunction

= (props: P) => Interpolation

; @@ -76,9 +85,8 @@ type DeprecatedAttrs, T> = { [K in keyof A]: ((props: ThemedStyledProps) => A[K]) | A[K] }; -export type ThemedGlobalStyledClassProps = P & { - suppressMultiMountWarning?: boolean - theme?: T +export type ThemedGlobalStyledClassProps = WithOptionalTheme & { + suppressMultiMountWarning?: boolean; }; export interface GlobalStyleComponent @@ -86,8 +94,8 @@ export interface GlobalStyleComponent // remove the call signature from StyledComponent so Interpolation can still infer InterpolationFunction type StyledComponentInterpolation = Pick< - StyledComponent, - keyof StyledComponent + AnyStyledComponent, + keyof StyledComponentBase >; // abuse Pick to strip the call signature from ForwardRefExoticComponent @@ -101,7 +109,16 @@ export type AnyStyledComponent = | StyledComponent | StyledComponent; -export interface StyledComponent< +export type StyledComponent< + C extends keyof JSX.IntrinsicElements | React.ComponentType, + T extends object, + O extends object = {}, + A extends keyof any = never +> = // the "string" allows this to be used as an object key + // I really want to avoid this if possible but it's the only way to use nesting with object styles... + string & StyledComponentBase; + +export interface StyledComponentBase< C extends keyof JSX.IntrinsicElements | React.ComponentType, T extends object, O extends object = {}, @@ -127,7 +144,7 @@ export interface StyledComponent< * * String types need to be cast to themselves to become literal types (as={'a' as 'a'}). */ - as?: keyof JSX.IntrinsicElements | React.ComponentType + as?: keyof JSX.IntrinsicElements | React.ComponentType; } ): React.ReactElement>; withComponent( @@ -145,34 +162,38 @@ export interface StyledComponent< ): StyledComponent; } +export type ThemedStyledFunctionBase< + C extends keyof JSX.IntrinsicElements | React.ComponentType, + T extends object, + O extends object = {}, + A extends keyof any = never +> = + // at least the first argument is required, whatever it is + ( + first: NonNullable< + Interpolation< + ThemedStyledProps & O & U, T> + > + >, + ...rest: Array< + Interpolation< + ThemedStyledProps & O & U, T> + > + > + ) => StyledComponent; + export interface ThemedStyledFunction< C extends keyof JSX.IntrinsicElements | React.ComponentType, T extends object, O extends object = {}, A extends keyof any = never -> { - ( - strings: TemplateStringsArray, - ...interpolations: Array< - Interpolation< - ThemedStyledProps & O, T> - > - > - ): StyledComponent; - ( - strings: TemplateStringsArray, - ...interpolations: Array< - Interpolation< - ThemedStyledProps & O & U, T> - > - > - ): StyledComponent; +> extends ThemedStyledFunctionBase { // Fun thing: 'attrs' can also provide a polymorphic 'as' prop // My head already hurts enough so maybe later... attrs< U, A extends Partial & U> & { - [others: string]: any + [others: string]: any; } = {} >( attrs: Attrs & U, A, T> @@ -183,7 +204,7 @@ export interface ThemedStyledFunction< attrs< U, A extends Partial & U> & { - [others: string]: any + [others: string]: any; } = {} >( attrs: DeprecatedAttrs & U, A, T> @@ -244,20 +265,11 @@ export type ThemedStyledInterface = ThemedBaseStyledInterface< >; export type StyledInterface = ThemedStyledInterface; -export interface BaseThemedCssFunction { - (cssObject: CSSObject): InterpolationValue[]; - ( - strings: TemplateStringsArray, - ...interpolations: SimpleInterpolation[] - ): InterpolationValue[]; -

( - strings: TemplateStringsArray, - ...interpolations: Array>> - ): Array>>; -

(func: InterpolationFunction>): Array< - FlattenInterpolation> - >; -} +export type BaseThemedCssFunction =

( + first: NonNullable>>, + ...interpolations: Array>> +) => Array>>; + export type ThemedCssFunction = BaseThemedCssFunction< Extract extends never ? any : T >; @@ -266,8 +278,8 @@ export type ThemedCssFunction = BaseThemedCssFunction< type Omit = Pick>; type DiffBetween = Pick> & Pick>; -type WithOptionalTheme

= Omit & { - theme?: T +type WithOptionalTheme

= Omit & { + theme?: T; }; export interface ThemedStyledComponentsModule { @@ -275,20 +287,16 @@ export interface ThemedStyledComponentsModule { css: ThemedCssFunction; - keyframes(cssObject: CSSObject): Keyframes; + // unfortunately keyframes can't interpolate props from the theme keyframes( - strings: TemplateStringsArray, + strings: TemplateStringsArray | CSSKeyframes, ...interpolations: SimpleInterpolation[] ): Keyframes; - createGlobalStyle(cssObject: CSSObject): GlobalStyleComponent<{}, T>; - createGlobalStyle

( - strings: TemplateStringsArray, + createGlobalStyle

( + first: NonNullable>>, ...interpolations: Array>> ): GlobalStyleComponent; - createGlobalStyle

( - func: InterpolationFunction> - ): GlobalStyleComponent; withTheme: WithThemeFnInterface; ThemeProvider: ThemeProviderComponent; @@ -326,33 +334,28 @@ export type BaseThemeProviderComponent = React.ComponentClass< >; export type ThemeProviderComponent< T extends object -> = BaseThemeProviderComponent extends never ? any : T>; +> = BaseThemeProviderComponent< + Extract extends never ? any : T +>; export const ThemeProvider: ThemeProviderComponent; // NOTE: this technically starts as undefined // Also, this cannot be DefaultTheme as it breaks TypedComponents' assignability export const ThemeContext: React.Context; -export const ThemeConsumer: typeof ThemeContext['Consumer']; +export const ThemeConsumer: typeof ThemeContext["Consumer"]; export interface Keyframes { getName(): string; } -export function keyframes(cssObject: CSSObject): Keyframes; export function keyframes( - strings: TemplateStringsArray, + strings: TemplateStringsArray | CSSKeyframes, ...interpolations: SimpleInterpolation[] ): Keyframes; -export function createGlobalStyle( - cssObject: CSSObject -): GlobalStyleComponent<{}, DefaultTheme>; -export function createGlobalStyle

( - strings: TemplateStringsArray, +export function createGlobalStyle

( + first: NonNullable>>, ...interpolations: Array>> ): GlobalStyleComponent; -export function createGlobalStyle

( - func: InterpolationFunction> -): GlobalStyleComponent; export function isStyledComponent( target: any @@ -373,12 +376,12 @@ export class ServerStyleSheet { type StyleSheetManagerProps = | { - sheet: ServerStyleSheet - target?: never + sheet: ServerStyleSheet; + target?: never; } | { - sheet?: never - target: HTMLElement + sheet?: never; + target: HTMLElement; }; export class StyleSheetManager extends React.Component< diff --git a/types/styled-components/test/index.tsx b/types/styled-components/test/index.tsx index 64e0f447ba..7ce9e59fa2 100644 --- a/types/styled-components/test/index.tsx +++ b/types/styled-components/test/index.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import * as ReactDOMServer from 'react-dom/server'; +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import * as ReactDOMServer from "react-dom/server"; import styled, { css, @@ -14,7 +14,8 @@ import styled, { withTheme, ThemeConsumer, StyledComponent, -} from 'styled-components'; + ThemedStyledComponentsModule +} from "styled-components"; /** * general usage @@ -72,8 +73,8 @@ const TomatoButton = styled(MyButton)` const CustomizableButton = styled(MyButton)` /* Adapt the colors based on primary prop */ - background: ${props => (props.primary ? 'palevioletred' : 'white')}; - color: ${props => (props.primary ? 'white' : 'palevioletred')}; + background: ${props => (props.primary ? "palevioletred" : "white")}; + color: ${props => (props.primary ? "white" : "palevioletred")}; font-size: 1em; margin: 1em; @@ -86,7 +87,7 @@ const example = css` font-size: 1.5em; text-align: center; color: ${props => props.theme.primary}; - border-color: ${'red'}; + border-color: ${"red"}; `; const fadeIn = keyframes` @@ -99,15 +100,15 @@ const fadeIn = keyframes` `; const animationRule = css` - ${fadeIn} 1s infinite alternate; + ${fadeIn} 1s infinite alternate; `; const ComponentWithKeyframe = styled.div` - animation: ${animationRule}; + animation: ${animationRule}; `; const theme = { - main: 'mediumseagreen', + main: "mediumseagreen" }; const ExampleGlobalStyle = createGlobalStyle` @@ -130,7 +131,7 @@ class Example extends React.Component { Hello World, this is my first styled component! - + @@ -143,13 +144,13 @@ class Example extends React.Component { // css which only uses simple interpolations without functions const cssWithValues1 = css` - font-size: ${14} ${'pt'}; + font-size: ${14} ${"pt"}; `; // css which uses other simple interpolations without functions const cssWithValues2 = css` ${cssWithValues1} ${[cssWithValues1, cssWithValues1]} - font-weight: ${'bold'}; + font-weight: ${"bold"}; `; // css which uses function interpolations with common props @@ -168,7 +169,7 @@ const styledButton = styled.button` ${() => [cssWithFunc1, cssWithFunc2]} `; -const name = 'hey'; +const name = "hey"; const ThemedMyButton = withTheme(MyButton); @@ -216,7 +217,7 @@ const ComposedLink = () => ( // Create a react component that renders an which is // centered, palevioletred and sized at 1.5em -const LinkFromString = styled('a')` +const LinkFromString = styled("a")` font-size: 1.5em; text-align: center; color: palevioletred; @@ -235,10 +236,10 @@ interface LinkProps { canClick: boolean; } -const LinkFromStringWithProps = styled('a')` +const LinkFromStringWithProps = styled("a")` font-size: 1.5em; text-align: center; - color: ${(a: LinkProps) => (a.canClick ? 'palevioletred' : 'gray')}; + color: ${(a: LinkProps) => (a.canClick ? "palevioletred" : "gray")}; `; // A LinkFromStringWithProps instance should be backed by an HTMLAnchorElement @@ -251,10 +252,10 @@ const MyOtherComponentWithProps = () => ( // Create a react component that renders an // which takes extra props passed as a generic type argument -const LinkFromStringWithPropsAndGenerics = styled('a')` +const LinkFromStringWithPropsAndGenerics = styled("a")` font-size: 1.5em; text-align: center; - color: ${a => (a.canClick ? 'palevioletred' : 'gray')}; + color: ${a => (a.canClick ? "palevioletred" : "gray")}; `; // A LinkFromStringWithPropsAndGenerics instance should be backed by an HTMLAnchorElement @@ -274,19 +275,19 @@ interface ObjectStyleProps { } const functionReturningStyleObject = (props: ObjectStyleProps) => ({ - padding: props.size === 'big' ? '10px' : 2, + padding: props.size === "big" ? "10px" : 2 }); const ObjectStylesBox = styled.div` ${functionReturningStyleObject} ${{ - backgroundColor: 'red', + backgroundColor: "red", // Supports nested objects (pseudo selectors, media queries, etc) - '@media screen and (min-width: 800px)': { - backgroundColor: 'blue', + "@media screen and (min-width: 800px)": { + backgroundColor: "blue" }, - fontSize: 2, + fontSize: 2 }}; `; @@ -298,11 +299,11 @@ const ObjectStylesBox = styled.div` const AttrsInput = styled.input.attrs({ // we can define static props - type: 'password', + type: "password", // or we can define dynamic ones - margin: (props: any) => (props.size as string) || '1em', - padding: (props: any) => (props.size as string) || '1em', + margin: (props: any) => (props.size as string) || "1em", + padding: (props: any) => (props.size as string) || "1em" })` color: palevioletred; font-size: 1em; @@ -315,9 +316,9 @@ const AttrsInput = styled.input.attrs({ `; // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30042 -const AttrsWithOnlyNewProps = styled.h2.attrs({ as: 'h1' })` - color: ${props => props.as === 'h1' ? 'red' : 'blue'}; - font-size: ${props => props.as === 'h1' ? 2 : 1}; +const AttrsWithOnlyNewProps = styled.h2.attrs({ as: "h1" })` + color: ${props => (props.as === "h1" ? "red" : "blue")}; + font-size: ${props => (props.as === "h1" ? 2 : 1)}; `; /** @@ -356,14 +357,14 @@ const ThemedButton = styled.button` // Define our `fg` and `bg` on the theme const theme2 = { - fg: 'palevioletred', - bg: 'white', + fg: "palevioletred", + bg: "white" }; // This theme swaps `fg` and `bg` const invertTheme = ({ fg, bg }: { fg: string; bg: string }) => ({ fg: bg, - bg: fg, + bg: fg }); const MyApp = ( @@ -386,7 +387,7 @@ class MyComponent extends React.Component> { render() { const { theme } = this.props; - console.log('Current theme: ', theme); + console.log("Current theme: ", theme); return

Hello

; } @@ -400,7 +401,7 @@ const ThemedMyComponent = withTheme(MyComponent); // ref; // }}/>; const themedRef = React.createRef(); -; +; interface WithThemeProps { theme: { @@ -415,12 +416,10 @@ const Component = (props: WithThemeProps) => ( const ComponentWithTheme = withTheme(Component); -; // ok -; // ok +; // ok +; // ok - - {(theme) => } -; +{theme => }; /** * isStyledComponent utility @@ -439,7 +438,7 @@ class ClassComponent extends React.Component { isStyledComponent(StyledComponent); isStyledComponent(StatelessComponent); isStyledComponent(ClassComponent); -isStyledComponent('div'); +isStyledComponent("div"); /** * server side rendering @@ -470,7 +469,7 @@ const css2 = sheet2.getStyleElement(); const sheet3 = new ServerStyleSheet(); const appStream = ReactDOMServer.renderToNodeStream(Hello world); const wrappedCssStream: NodeJS.ReadableStream = sheet3.interleaveWithNodeStream( - appStream, + appStream ); /** @@ -511,10 +510,10 @@ class Random extends React.Component { } } -const WithComponentH2 = WithComponentH1.withComponent('h2'); -const WithComponentAbbr = WithComponentH1.withComponent('abbr'); +const WithComponentH2 = WithComponentH1.withComponent("h2"); +const WithComponentAbbr = WithComponentH1.withComponent("abbr"); -const WithComponentAnchor = WithComponentH1.withComponent('a'); +const WithComponentAnchor = WithComponentH1.withComponent("a"); const AnchorContainer = () => ( withComponent Anchor @@ -524,37 +523,41 @@ const AnchorContainer = () => ( const WithComponentRandomHeading = WithComponentH1.withComponent(Random); const WithComponentCompA: React.SFC<{ a: number; className?: string }> = ({ - className, + className }) =>
; const WithComponentCompB: React.SFC<{ b: number; className?: string }> = ({ - className, + className }) =>
; const WithComponentStyledA = styled(WithComponentCompA)` color: ${(props: { color: string }) => props.color}; `; const WithComponentFirstStyledA = styled(WithComponentStyledA).attrs({ - a: 1, + a: 1 })``; const WithComponentFirstStyledB = WithComponentFirstStyledA.withComponent( - WithComponentCompB, + WithComponentCompB ); -const WithComponentFirstStyledANew = styled(WithComponentStyledA).attrs(props => ({ a: 1 }))``; +const WithComponentFirstStyledANew = styled(WithComponentStyledA).attrs( + props => ({ a: 1 }) +)``; const test = () => [ - , - , - , + , + , + ]; -const WithComponentRequired = styled((props: { to: string }) => )``; +const WithComponentRequired = styled((props: { to: string }) => ( + +))``; // These tests pass in tsservice, but they fail in dtslint. I do not know why. // ; // $ExpectError // ; -const WithComponentRequired2 = WithComponentRequired.withComponent('a'); +const WithComponentRequired2 = WithComponentRequired.withComponent("a"); // These tests pass in tsservice, but they fail in dtslint. I do not know why. // ; // ; // $ExpectError @@ -569,7 +572,7 @@ const asTest = ( ); interface TestContainerProps { - size: 'big' | 'small'; + size: "big" | "small"; test?: boolean; } const TestContainer = ({ size, test }: TestContainerProps) => { @@ -581,7 +584,7 @@ const StyledTestContainer = styled(TestContainer)` `; interface Test2ContainerProps { - type: 'foo' | 'bar'; + type: "foo" | "bar"; } class Test2Container extends React.Component { render() { @@ -592,28 +595,120 @@ class Test2Container extends React.Component { const containerTest = ( // TODO (TypeScript 3.2): once the polymorphic overload is un-commented-out this should be the correct test // - + ); // 4.0 refs -const divFnRef = (ref: HTMLDivElement|null) => { /* empty */ }; +const divFnRef = (ref: HTMLDivElement | null) => { + /* empty */ +}; const divRef = React.createRef(); const StyledDiv = styled.div``; -; -; -; // $ExpectError +; +; +; // $ExpectError const StyledStyledDiv = styled(StyledDiv)``; -; -; -; // $ExpectError +; +; +; // $ExpectError -const StyledA = StyledDiv.withComponent('a'); -; // $ExpectError - { - // $ExpectType HTMLAnchorElement | null - ref; -}}/>; +const StyledA = StyledDiv.withComponent("a"); +; // $ExpectError + { + // $ExpectType HTMLAnchorElement | null + ref; + }} +/>; + +async function typedThemes() { + const theme = { + color: "green" + }; + + // abuse "await import(...)" to be able to reference the styled-components namespace + // without actually doing a top level namespace import + const { + default: styled, + css, + createGlobalStyle, + ThemeProvider, + ThemeConsumer + } = (await import("styled-components")) as ThemedStyledComponentsModule< + typeof theme + >; + + const ThemedDiv = styled.div` + background: ${props => { + // $ExpectType string + props.theme.color; + // $ExpectType number | undefined + props.tabIndex; + return props.theme.color; + }}; + `; + const ThemedDiv2 = styled.div(props => { + // $ExpectType string + props.theme.color; + // $ExpectType number | undefined + props.tabIndex; + + return { + background: props.theme.color + }; + }); + const ThemedDiv3 = styled.div(props => { + // $ExpectType string + props.theme.color; + // $ExpectType number | undefined + props.tabIndex; + + return css` + background: ${props.theme.color}; + `; + }); + const themedCss = css` + background: ${props => { + // $ExpectType string + props.theme.color; + // $ExpectType "theme" + type Keys = keyof typeof props; + return props.theme.color; + }}; + `; + const ThemedDiv4 = styled.div(themedCss); + + const themedCssWithNesting = css(props => ({ + color: props.theme.color, + [ThemedDiv3]: { + color: "green" + } + })); + + const Global = createGlobalStyle` + ${themedCssWithNesting} + `; + + return ( + + <> + + + + + + + {theme => { + // $ExpectType string + theme.color; + return theme.color; + }} + + + + ); +} From 160c4de7df14c392722aed3fcdd931f53dbc4dd7 Mon Sep 17 00:00:00 2001 From: Jessica Date: Wed, 14 Nov 2018 17:25:01 +0900 Subject: [PATCH 2/6] Use a helper for the Extract stuff, support DefaultTheme in ThemeConsumer --- types/styled-components/index.d.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/types/styled-components/index.d.ts b/types/styled-components/index.d.ts index 6398ee36d5..c04de160b0 100644 --- a/types/styled-components/index.d.ts +++ b/types/styled-components/index.d.ts @@ -261,7 +261,7 @@ export interface ThemedBaseStyledInterface } export type ThemedStyledInterface = ThemedBaseStyledInterface< - Extract extends never ? any : T + AnyIfEmpty >; export type StyledInterface = ThemedStyledInterface; @@ -271,16 +271,15 @@ export type BaseThemedCssFunction =

( ) => Array>>; export type ThemedCssFunction = BaseThemedCssFunction< - Extract extends never ? any : T + AnyIfEmpty >; // Helper type operators type Omit = Pick>; -type DiffBetween = Pick> & - Pick>; type WithOptionalTheme

= Omit & { theme?: T; }; +type AnyIfEmpty = keyof T extends never ? any : T; export interface ThemedStyledComponentsModule { default: ThemedStyledInterface; @@ -318,7 +317,7 @@ export type BaseWithThemeFnInterface = < WithOptionalTheme, T> >; export type WithThemeFnInterface = BaseWithThemeFnInterface< - Extract extends never ? any : T + AnyIfEmpty >; export const withTheme: WithThemeFnInterface; @@ -334,13 +333,10 @@ export type BaseThemeProviderComponent = React.ComponentClass< >; export type ThemeProviderComponent< T extends object -> = BaseThemeProviderComponent< - Extract extends never ? any : T ->; -export const ThemeProvider: ThemeProviderComponent; -// NOTE: this technically starts as undefined -// Also, this cannot be DefaultTheme as it breaks TypedComponents' assignability -export const ThemeContext: React.Context; +> = BaseThemeProviderComponent>; +export const ThemeProvider: ThemeProviderComponent>; +// NOTE: this technically starts as undefined, but allowing undefined is unhelpful when used correctly +export const ThemeContext: React.Context>; export const ThemeConsumer: typeof ThemeContext["Consumer"]; export interface Keyframes { From 9a4b5d4d34c079b80117f4c3758afce64a506e49 Mon Sep 17 00:00:00 2001 From: Jessica Date: Wed, 14 Nov 2018 17:37:42 +0900 Subject: [PATCH 3/6] Document DefaultTheme, test ThemedStyledComponentsModule shape --- types/styled-components/index.d.ts | 12 ++++++++++++ types/styled-components/test/index.tsx | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/types/styled-components/index.d.ts b/types/styled-components/index.d.ts index c04de160b0..2ad44e7e57 100644 --- a/types/styled-components/index.d.ts +++ b/types/styled-components/index.d.ts @@ -301,6 +301,12 @@ export interface ThemedStyledComponentsModule { ThemeProvider: ThemeProviderComponent; ThemeConsumer: React.Consumer; ThemeContext: React.Context; + + // This could be made to assert `target is StyledComponent` instead, but that feels not type safe + isStyledComponent: typeof isStyledComponent; + + ServerStyleSheet: typeof ServerStyleSheet; + StyleSheetManager: typeof StyleSheetManager; } declare const styled: StyledInterface; @@ -321,6 +327,12 @@ export type WithThemeFnInterface = BaseWithThemeFnInterface< >; export const withTheme: WithThemeFnInterface; +/** + * This interface can be augmented by users to add types to `styled-components`' default theme + * without needing to reexport `ThemedStyledComponentsModule`. + */ +// Unfortunately, there is no way to write tests for this +// as any augmentation will break the tests for the default case (not augmented). // tslint:disable-next-line:no-empty-interface export interface DefaultTheme {} diff --git a/types/styled-components/test/index.tsx b/types/styled-components/test/index.tsx index 7ce9e59fa2..628a294a48 100644 --- a/types/styled-components/test/index.tsx +++ b/types/styled-components/test/index.tsx @@ -712,3 +712,17 @@ async function typedThemes() { ); } + +async function reexportCompatibility() { + const sc = await import("styled-components"); + const themed = sc as ThemedStyledComponentsModule; + + let { ...scExports } = sc; + let { ...themedExports } = themed; + // both branches must be assignable to each other + if (Math.random()) { + scExports = themedExports; + } else { + themedExports = scExports; + } +} From 9e6ea6abc62b5e8e0fe73c3b60e57460f153975c Mon Sep 17 00:00:00 2001 From: Jessica Date: Wed, 14 Nov 2018 18:53:20 +0900 Subject: [PATCH 4/6] Add a way to declare a ThemeProvider is augmenting another ThemeProvider --- types/styled-components/index.d.ts | 23 +++++++++------ types/styled-components/test/index.tsx | 41 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/types/styled-components/index.d.ts b/types/styled-components/index.d.ts index 2ad44e7e57..b915040c7e 100644 --- a/types/styled-components/index.d.ts +++ b/types/styled-components/index.d.ts @@ -281,7 +281,10 @@ type WithOptionalTheme

= Omit & { }; type AnyIfEmpty = keyof T extends never ? any : T; -export interface ThemedStyledComponentsModule { +export interface ThemedStyledComponentsModule< + T extends object, + U extends object = T +> { default: ThemedStyledInterface; css: ThemedCssFunction; @@ -298,7 +301,7 @@ export interface ThemedStyledComponentsModule { ): GlobalStyleComponent; withTheme: WithThemeFnInterface; - ThemeProvider: ThemeProviderComponent; + ThemeProvider: ThemeProviderComponent; ThemeConsumer: React.Consumer; ThemeContext: React.Context; @@ -336,16 +339,18 @@ export const withTheme: WithThemeFnInterface; // tslint:disable-next-line:no-empty-interface export interface DefaultTheme {} -export interface ThemeProviderProps { +export interface ThemeProviderProps { children?: React.ReactChild; // only one child is allowed, goes through React.Children.only - theme: T | ((theme: T) => T); + theme: T | ((theme: U) => T); } -export type BaseThemeProviderComponent = React.ComponentClass< - ThemeProviderProps ->; +export type BaseThemeProviderComponent< + T extends object, + U extends object = T +> = React.ComponentClass>; export type ThemeProviderComponent< - T extends object -> = BaseThemeProviderComponent>; + T extends object, + U extends object = T +> = BaseThemeProviderComponent, AnyIfEmpty>; export const ThemeProvider: ThemeProviderComponent>; // NOTE: this technically starts as undefined, but allowing undefined is unhelpful when used correctly export const ThemeContext: React.Context>; diff --git a/types/styled-components/test/index.tsx b/types/styled-components/test/index.tsx index 628a294a48..0e9fec66a1 100644 --- a/types/styled-components/test/index.tsx +++ b/types/styled-components/test/index.tsx @@ -726,3 +726,44 @@ async function reexportCompatibility() { themedExports = scExports; } } + +async function themeAugmentation() { + interface BaseTheme { + background: string; + } + interface ExtraTheme extends BaseTheme { + accent: string; + } + + const base = (await import("styled-components")) as ThemedStyledComponentsModule< + BaseTheme + >; + const extra = (await import("styled-components")) as ThemedStyledComponentsModule< + ExtraTheme, + BaseTheme + >; + + return ( + + <> + base} // $ExpectError + > + {() => null} + + ({ + ...base, + accent: "blue" + })} + > + {() => null} + + + + ); +} From 2ee57c7d32b0ef6af3d7d5197a924fc8763c7365 Mon Sep 17 00:00:00 2001 From: Jessica Date: Fri, 16 Nov 2018 10:54:52 +0900 Subject: [PATCH 5/6] Fix return type of css``, tweak overloads --- types/styled-components/index.d.ts | 89 +++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/types/styled-components/index.d.ts b/types/styled-components/index.d.ts index b915040c7e..4a86aa245f 100644 --- a/types/styled-components/index.d.ts +++ b/types/styled-components/index.d.ts @@ -9,8 +9,8 @@ /// -import * as React from "react"; import * as CSS from "csstype"; +import * as React from "react"; export type CSSObject = CSS.Properties & // Index type to allow selector nesting @@ -30,7 +30,7 @@ export interface ThemeProps { } export type ThemedStyledProps = P & ThemeProps; -export type StyledProps

= ThemedStyledProps; +export type StyledProps

= ThemedStyledProps>; export type StyledComponentProps< // The Component from whose props are derived @@ -57,8 +57,8 @@ type StyledComponentPropsWithAs< export type FalseyValue = undefined | null | false; export type Interpolation

= | InterpolationValue - | InterpolationFunction

- | FlattenInterpolation

; + | FlattenInterpolation

+ | InterpolationFunction

; // must be an interface to be self-referential export interface FlattenInterpolation

extends ReadonlyArray> {} @@ -93,10 +93,15 @@ export interface GlobalStyleComponent extends React.ComponentClass> {} // remove the call signature from StyledComponent so Interpolation can still infer InterpolationFunction -type StyledComponentInterpolation = Pick< - AnyStyledComponent, - keyof StyledComponentBase ->; +type StyledComponentInterpolation = + | Pick< + StyledComponentBase, + keyof StyledComponentBase + > + | Pick< + StyledComponentBase, + keyof StyledComponentBase + >; // abuse Pick to strip the call signature from ForwardRefExoticComponent type ForwardRefExoticBase

= Pick< @@ -162,25 +167,45 @@ export interface StyledComponentBase< ): StyledComponent; } -export type ThemedStyledFunctionBase< +export interface ThemedStyledFunctionBase< C extends keyof JSX.IntrinsicElements | React.ComponentType, T extends object, O extends object = {}, A extends keyof any = never -> = +> { + ( + first: + | TemplateStringsArray + | NonNullable< + Interpolation< + ThemedStyledProps & O, T> + > + >, + ...rest: Array< + Interpolation< + ThemedStyledProps & O, T> + > + > + ): StyledComponent; // at least the first argument is required, whatever it is ( - first: NonNullable< - Interpolation< - ThemedStyledProps & O & U, T> - > - >, + first: + | TemplateStringsArray + | NonNullable< + Interpolation< + ThemedStyledProps< + StyledComponentPropsWithRef & O & U, + T + > + > + >, ...rest: Array< Interpolation< ThemedStyledProps & O & U, T> > > - ) => StyledComponent; + ): StyledComponent; +} export interface ThemedStyledFunction< C extends keyof JSX.IntrinsicElements | React.ComponentType, @@ -243,17 +268,13 @@ type StyledComponentInnerAttrs< export interface ThemedBaseStyledInterface extends ThemedStyledComponentFactories { - (tag: TTag): ThemedStyledFunction< - TTag, - T - >; (component: C): ThemedStyledFunction< StyledComponentInnerComponent, T, StyledComponentInnerOtherProps, StyledComponentInnerAttrs >; - >( + >( // unfortunately using a conditional type to validate that it can receive a `theme?: Theme` // causes tests to fail in TS 3.1 component: C @@ -265,10 +286,20 @@ export type ThemedStyledInterface = ThemedBaseStyledInterface< >; export type StyledInterface = ThemedStyledInterface; -export type BaseThemedCssFunction =

( - first: NonNullable>>, - ...interpolations: Array>> -) => Array>>; +export interface BaseThemedCssFunction { + ( + first: + | TemplateStringsArray + | NonNullable>>, + ...interpolations: Array>> + ): FlattenInterpolation>; +

( + first: + | TemplateStringsArray + | NonNullable>>, + ...interpolations: Array>> + ): FlattenInterpolation>; +} export type ThemedCssFunction = BaseThemedCssFunction< AnyIfEmpty @@ -296,7 +327,9 @@ export interface ThemedStyledComponentsModule< ): Keyframes; createGlobalStyle

( - first: NonNullable>>, + first: + | TemplateStringsArray + | NonNullable>>, ...interpolations: Array>> ): GlobalStyleComponent; @@ -366,7 +399,9 @@ export function keyframes( ): Keyframes; export function createGlobalStyle

( - first: NonNullable>>, + first: + | TemplateStringsArray + | NonNullable>>, ...interpolations: Array>> ): GlobalStyleComponent; From a0a99124e9c77cac8e3985001a3865f47d6bf6fa Mon Sep 17 00:00:00 2001 From: Jessica Date: Fri, 16 Nov 2018 16:54:58 +0900 Subject: [PATCH 6/6] Preserve optionality of parent StyledComponent .attrs --- types/styled-components/index.d.ts | 12 ++++++------ types/styled-components/test/index.tsx | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/types/styled-components/index.d.ts b/types/styled-components/index.d.ts index 4a86aa245f..a842c94c63 100644 --- a/types/styled-components/index.d.ts +++ b/types/styled-components/index.d.ts @@ -217,23 +217,23 @@ export interface ThemedStyledFunction< // My head already hurts enough so maybe later... attrs< U, - A extends Partial & U> & { + NewA extends Partial & U> & { [others: string]: any; } = {} >( - attrs: Attrs & U, A, T> - ): ThemedStyledFunction; + attrs: Attrs & U, NewA, T> + ): ThemedStyledFunction; // Only this overload is deprecated // tslint:disable:unified-signatures /** @deprecated Prefer using the new single function style, to be removed in v5 */ attrs< U, - A extends Partial & U> & { + NewA extends Partial & U> & { [others: string]: any; } = {} >( - attrs: DeprecatedAttrs & U, A, T> - ): ThemedStyledFunction; + attrs: DeprecatedAttrs & U, NewA, T> + ): ThemedStyledFunction; // tslint:enable:unified-signatures } diff --git a/types/styled-components/test/index.tsx b/types/styled-components/test/index.tsx index 0e9fec66a1..e435d90011 100644 --- a/types/styled-components/test/index.tsx +++ b/types/styled-components/test/index.tsx @@ -321,6 +321,10 @@ const AttrsWithOnlyNewProps = styled.h2.attrs({ as: "h1" })` font-size: ${props => (props.as === "h1" ? 2 : 1)}; `; +const AttrsInputExtra = styled(AttrsInput).attrs({ autoComplete: "off" })``; + +; + /** * component type */