diff --git a/types/next/app.d.ts b/types/next/app.d.ts index d077c0e95a..aef2980063 100644 --- a/types/next/app.d.ts +++ b/types/next/app.d.ts @@ -1,19 +1,46 @@ import * as React from "react"; -import { NextContext } from "."; +import { NextContext, NextComponentType } from "."; import { RouterProps, DefaultQuery } from "./router"; -export interface AppComponentProps { - Component: React.ComponentType; - router: RouterProps; - pageProps: any; -} +// Deprecated +export type AppComponentProps = AppProps; +export type AppComponentContext = NextAppContext; +// End Deprecated -export interface AppComponentContext { - Component: React.ComponentType; +/** + * Context passed to App.getInitialProps. + * Component is dynamic - cannot infer type. + * + * @template Q Query object schema. + */ +export interface NextAppContext { + Component: NextComponentType>; router: RouterProps; ctx: NextContext; } -export class Container extends React.Component { } +/** + * Props passed to the App component. + * Component and pageProps are dynamic - cannot infer type. + * + * @template Q Query object schema. + */ +export interface AppProps { + Component: NextComponentType>; + router: RouterProps; + pageProps: any; +} -export default class App extends React.Component> { } +/** + * App component type. Differs from the default type because the context it passes + * to getInitialProps and the props is passes to the component are different. + * + * @template IP Initial props returned from getInitialProps. + * @template C Context passed to getInitialProps. + */ +export type AppComponentType = NextComponentType; + +export class Container extends React.Component {} +export default class App extends React.Component { + getInitialProps(context: C): Promise | IP; +} diff --git a/types/next/document.d.ts b/types/next/document.d.ts index e6ba38b6c9..2e9880db51 100644 --- a/types/next/document.d.ts +++ b/types/next/document.d.ts @@ -1,6 +1,6 @@ import * as React from "react"; - -import { NextContext } from "."; +import { NextContext, NextComponentType } from "."; +import { DefaultQuery } from "./router"; export interface RenderPageResponse { buildManifest: { [key: string]: any }; @@ -21,16 +21,25 @@ export interface AnyPageProps extends PageProps { [key: string]: any; } -export type Enhancer = (page: React.ComponentType

) => React.ComponentType; +export type Enhancer = ( + page: React.ComponentType

+) => React.ComponentType; /** - * Context object used inside `Document` + * Context passed to Document.getInitialProps. + * + * @template Q Query object schema. */ -export interface NextDocumentContext extends NextContext { +export interface NextDocumentContext extends NextContext { /** A callback that executes the actual React rendering logic (synchronously) */ - renderPage(enhancer?: Enhancer): RenderPageResponse; // tslint:disable-line:no-unnecessary-generics + renderPage( + enhancer?: Enhancer // tslint:disable-line no-unnecessary-generics + ): RenderPageResponse; } +/** + * Props passed to the Document component. + */ export interface DocumentProps { __NEXT_DATA__?: any; dev?: boolean; @@ -45,18 +54,40 @@ export interface DocumentProps { [key: string]: any; } +/** + * Props supported by the Head component. + */ export interface HeadProps { nonce?: string; [key: string]: any; } +/** + * Props supported by the NextScript component. + */ export interface NextScriptProps { nonce?: string; + [key: string]: any; } +/** + * Document component type. Differs from the default type because the context it passes + * to getInitialProps and the props is passes to the component are different. + * + * @template IP Initial props returned from getInitialProps. + * @template C Context passed to getInitialProps. + */ +export type DocumentComponentType = NextComponentType< + IP & DocumentProps, + IP, + C +>; + export class Head extends React.Component {} export class Main extends React.Component {} export class NextScript extends React.Component {} -export default class Document extends React.Component { - static getInitialProps(ctx: NextDocumentContext): Promise | DocumentProps; +export default class Document extends React.Component< + IP & DocumentProps +> { + getInitialProps(context: C): Promise | IP; } diff --git a/types/next/index.d.ts b/types/next/index.d.ts index 08e82106ab..c66e4c82ac 100644 --- a/types/next/index.d.ts +++ b/types/next/index.d.ts @@ -16,16 +16,22 @@ import * as url from "url"; import { Response as NodeResponse } from "node-fetch"; -import { SingletonRouter, DefaultQuery } from './router'; +import { SingletonRouter, DefaultQuery } from "./router"; declare namespace next { // Deprecated type QueryStringMapObject = DefaultQuery; + type ServerConfig = NextConfig; + // End Deprecated + + type UrlLike = url.UrlObject | url.Url; /** * Context object used in methods like `getInitialProps()` * https://github.com/zeit/next.js/blob/6.1.1/server/render.js#L77 * https://github.com/zeit/next.js/blob/6.1.1/readme.md#fetching-data-and-component-lifecycle + * + * @template Q Query object schema. */ interface NextContext { /** path section of URL */ @@ -44,19 +50,11 @@ declare namespace next { err?: Error; } - type NextSFC = NextStatelessComponent; - interface NextStatelessComponent - extends React.StatelessComponent { - getInitialProps?: (ctx: NextContext) => Promise; - } - - type UrlLike = url.UrlObject | url.Url; - /** * Next.js config schema. * https://github.com/zeit/next.js/blob/6.1.1/server/config.js#L10 */ - interface ServerConfig { + interface NextConfig { webpack?: any; webpackDevMiddleware?: any; poweredByHeader?: boolean; @@ -83,7 +81,7 @@ declare namespace next { dev?: boolean; staticMarkup?: boolean; quiet?: boolean; - conf?: ServerConfig; + conf?: NextConfig; } /** @@ -97,7 +95,7 @@ declare namespace next { quiet: boolean; router: SingletonRouter; http: null | http.Server; - nextConfig: ServerConfig; + nextConfig: NextConfig; distDir: string; buildId: string; hotReloader: any; @@ -114,7 +112,7 @@ declare namespace next { getHotReloader( dir: string, - options: { quiet: boolean; config: ServerConfig; buildId: string } + options: { quiet: boolean; config: NextConfig; buildId: string } ): any; handleRequest( req: http.IncomingMessage, @@ -132,11 +130,7 @@ declare namespace next { close(): Promise; defineRoutes(): Promise; start(): Promise; - run( - req: http.IncomingMessage, - res: http.ServerResponse, - parsedUrl: UrlLike - ): Promise; + run(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: UrlLike): Promise; render( req: http.IncomingMessage, @@ -182,6 +176,49 @@ declare namespace next { getCompilationError(): Promise; send404(res: http.ServerResponse): void; } + + /** + * Next.js counterpart of React.ComponentType. + * Specially useful in HOCs that receive Next.js components. + * + * @template P Component props. + * @template IP Initial props returned from getInitialProps. + * @template C Context passed to getInitialProps. + */ + type NextComponentType

= + | NextComponentClass + | NextStatelessComponent; + + /** + * Next.js counterpart of React.SFC/React.StatelessComponent. + * + * @template P Component props. + * @template IP Initial props returned from getInitialProps. + * @template C Context passed to getInitialProps. + */ + type NextSFC

= NextStatelessComponent; + type NextStatelessComponent

= React.StatelessComponent

& + NextStaticLifecycle; + + /** + * Next.js counterpart of React.ComponentClass. + * + * @template P Component props. + * @template IP Initial props returned from getInitialProps. + * @template C Context passed to getInitialProps. + */ + type NextComponentClass

= React.ComponentClass

& + NextStaticLifecycle; + + /** + * Next.js specific lifecycle methods. + * + * @template IP Initial props returned from getInitialProps and passed to the component. + * @template C Context passed to getInitialProps. + */ + interface NextStaticLifecycle { + getInitialProps?: (ctx: C) => Promise | IP; + } } declare function next(options?: next.ServerOptions): next.Server; diff --git a/types/next/test/next-app-tests.tsx b/types/next/test/next-app-tests.tsx index ffdcbaaf65..331df9e92f 100644 --- a/types/next/test/next-app-tests.tsx +++ b/types/next/test/next-app-tests.tsx @@ -1,24 +1,15 @@ import * as React from "react"; -import App, { Container, AppComponentContext } from "next/app"; -import { NextStatelessComponent } from "next"; +import App, { Container, NextAppContext, AppProps, AppComponentType } from "next/app"; interface NextComponentProps { example: string; } -class TestApp extends App { - static async getInitialProps({ Component, router, ctx }: AppComponentContext) { - let pageProps = {}; - // TODO: fix AppComponentContext to return NextComponentType instead of React.ComponentType - const Page = Component as NextStatelessComponent; - - if (Page.getInitialProps) { - pageProps = await Page.getInitialProps(ctx); - } - - return { pageProps }; - } +interface TypedQuery { + id?: string; +} +class TestApp extends App { render() { const { Component, router, pageProps } = this.props; return ( @@ -28,3 +19,78 @@ class TestApp extends App { ); } } + +class TestAppWithProps extends App { + static async getInitialProps({ Component, router, ctx }: NextAppContext) { + const pageProps = Component.getInitialProps && (await Component.getInitialProps(ctx)); + return { pageProps, example: "foobar" }; + } + + render() { + const { Component, router, pageProps, example } = this.props; + return ; + } +} + +class TestAppWithTypedQuery extends App<{}, NextAppContext> { + static async getInitialProps({ ctx }: NextAppContext) { + const { id } = ctx.query; + const processQuery = (id?: string) => id; + processQuery(id); + } +} + +interface WithExampleProps { + example: string; +} + +interface WithExampleHocProps { + test: string; +} + +interface TestProps { + ownProp: boolean; +} + +// Stateful HOC that adds props to wrapped component. Similar to what withRedux does. +// tslint:disable-next-line no-unnecessary-generics +const withExample =

(App: AppComponentType

) => + class extends React.Component

{ + test: string; + + static async getInitialProps(context: NextAppContext) { + const pageProps = App.getInitialProps && (await App.getInitialProps(context)); + + // tslint:disable-next-line prefer-object-spread + return Object.assign({}, pageProps, { test: "test" }); + } + + constructor(props: P & AppProps & WithExampleHocProps) { + super(props); + this.test = props.test; + } + + render() { + return ; + } + }; + +// Basic stateless HOC. Similar to what withAuth would do. +// tslint:disable-next-line no-unnecessary-generics +const withBasic =

(App: AppComponentType) => + class extends React.Component

{ + static async getInitialProps(context: C) { + const pageProps = App.getInitialProps && (await App.getInitialProps(context)); + + // tslint:disable-next-line prefer-object-spread + return Object.assign({}, pageProps); + } + + render() { + return ; + } + }; + +withExample(TestAppWithProps); + +withBasic(TestAppWithTypedQuery); diff --git a/types/next/test/next-component-tests.tsx b/types/next/test/next-component-tests.tsx index 37cd7264ef..4ba145ee15 100644 --- a/types/next/test/next-component-tests.tsx +++ b/types/next/test/next-component-tests.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { NextStatelessComponent, NextContext } from "next"; +import { NextStatelessComponent, NextContext, NextComponentType } from "next"; interface NextComponentProps { example: string; @@ -12,13 +12,11 @@ interface TypedQuery { class ClassNext extends React.Component { static async getInitialProps(ctx: NextContext) { const { example } = ctx.query; - return { example }; + return { example: example as string }; } render() { - return ( -

I'm a class component! {this.props.example}
- ); + return
I'm a class component! {this.props.example}
; } } @@ -38,3 +36,87 @@ StatelessNext.getInitialProps = async ({ query }: NextContext) => { const { example } = query; return { example: example as string }; }; + +interface WithExampleProps { + example: string; +} + +interface WithExampleHocProps { + test: string; +} + +interface TestProps { + ownProp: boolean; +} + +// Stateful HOC that adds props to wrapped component. Similar to what withRedux does. +// tslint:disable-next-line use-default-type-parameter +const withExample =

(Page: NextComponentType

) => + class extends React.Component

{ + test: string; + + static async getInitialProps(ctx: NextContext) { + const pageProps = Page.getInitialProps && (await Page.getInitialProps(ctx)); + + // tslint:disable-next-line prefer-object-spread + return Object.assign({}, pageProps, { test: "test" }); + } + + constructor(props: P & WithExampleHocProps) { + super(props); + this.test = props.test; + } + + render() { + return ; + } + }; + +// Basic stateless HOC. Similar to what withAuth would do. +// tslint:disable-next-line no-unnecessary-generics +const withBasic =

(Page: NextComponentType

) => + class extends React.Component

{ + static async getInitialProps(ctx: NextContext) { + const pageProps = Page.getInitialProps && (await Page.getInitialProps(ctx)); + + // tslint:disable-next-line prefer-object-spread + const props = Object.assign({}, pageProps); + + if (ctx.query.example === "bar") { + // Redirect + } + + return props; + } + + render() { + return ; + } + }; + +class NextWithExample extends React.Component { + static async getInitialProps(ctx: NextContext) { + const { id } = ctx.query; + const processQuery = (id?: string) => id; + processQuery(id); + return { ownProp: true }; + } + + render() { + const { ownProp, example } = this.props; + return ( +

+ I'm wrapped in a HOC that gives me an example prop! {example} {ownProp} +
+ ); + } +} + +// P template is inferred as . +withExample(NextWithExample); + +// P template inferred as . +withBasic(ClassNext); + +// P template inferred as +withBasic(withExample(NextWithExample)); diff --git a/types/zeit__next-typescript/zeit__next-typescript-tests.ts b/types/zeit__next-typescript/zeit__next-typescript-tests.ts index a7b58c46fc..6a9a87ac50 100644 --- a/types/zeit__next-typescript/zeit__next-typescript-tests.ts +++ b/types/zeit__next-typescript/zeit__next-typescript-tests.ts @@ -1,3 +1,3 @@ import withTypescript = require('@zeit/next-typescript'); -withTypescript({}); // $ExpectType ServerConfig +withTypescript({}); // $ExpectType NextConfig