Merge pull request #29021 from joaovieira/next/component-types

[@types/next] Add NextComponentType akin to React.ComponentType.
This commit is contained in:
Jesse Trinity
2018-09-20 16:43:31 -07:00
committed by GitHub
6 changed files with 299 additions and 56 deletions

47
types/next/app.d.ts vendored
View File

@@ -1,19 +1,46 @@
import * as React from "react";
import { NextContext } from ".";
import { NextContext, NextComponentType } from ".";
import { RouterProps, DefaultQuery } from "./router";
export interface AppComponentProps<Q = DefaultQuery> {
Component: React.ComponentType<any>;
router: RouterProps<Q>;
pageProps: any;
}
// Deprecated
export type AppComponentProps = AppProps;
export type AppComponentContext = NextAppContext;
// End Deprecated
export interface AppComponentContext<Q = DefaultQuery> {
Component: React.ComponentType<any>;
/**
* Context passed to App.getInitialProps.
* Component is dynamic - cannot infer type.
*
* @template Q Query object schema.
*/
export interface NextAppContext<Q = DefaultQuery> {
Component: NextComponentType<any, any, NextContext<Q>>;
router: RouterProps<Q>;
ctx: NextContext<Q>;
}
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<Q = DefaultQuery> {
Component: NextComponentType<any, any, NextContext<Q>>;
router: RouterProps<Q>;
pageProps: any;
}
export default class App<TProps = {}, Q = DefaultQuery> extends React.Component<TProps & AppComponentProps<Q>> { }
/**
* 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<IP = {}, C = NextAppContext> = NextComponentType<IP & AppProps, IP, C>;
export class Container extends React.Component {}
export default class App<IP = {}, C = NextAppContext> extends React.Component<IP & AppProps> {
getInitialProps(context: C): Promise<IP> | IP;
}

View File

@@ -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<E extends PageProps = AnyPageProps, P extends any = E> = (page: React.ComponentType<P>) => React.ComponentType<E>;
export type Enhancer<E extends PageProps = AnyPageProps, P extends any = E> = (
page: React.ComponentType<P>
) => React.ComponentType<E>;
/**
* Context object used inside `Document`
* Context passed to Document.getInitialProps.
*
* @template Q Query object schema.
*/
export interface NextDocumentContext extends NextContext {
export interface NextDocumentContext<Q = DefaultQuery> extends NextContext<Q> {
/** A callback that executes the actual React rendering logic (synchronously) */
renderPage<E extends PageProps = AnyPageProps, P extends any = E>(enhancer?: Enhancer<E, P>): RenderPageResponse; // tslint:disable-line:no-unnecessary-generics
renderPage<E extends PageProps = AnyPageProps, P extends any = E>(
enhancer?: Enhancer<E, P> // 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<IP = {}, C = NextDocumentContext> = NextComponentType<
IP & DocumentProps,
IP,
C
>;
export class Head extends React.Component<HeadProps> {}
export class Main extends React.Component {}
export class NextScript extends React.Component<NextScriptProps> {}
export default class Document<TProps = {}> extends React.Component<TProps & DocumentProps> {
static getInitialProps(ctx: NextDocumentContext): Promise<DocumentProps> | DocumentProps;
export default class Document<IP = {}, C = NextDocumentContext> extends React.Component<
IP & DocumentProps
> {
getInitialProps(context: C): Promise<IP> | IP;
}

73
types/next/index.d.ts vendored
View File

@@ -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<Q = DefaultQuery> {
/** path section of URL */
@@ -44,19 +50,11 @@ declare namespace next {
err?: Error;
}
type NextSFC<TProps = {}, Q = DefaultQuery> = NextStatelessComponent<TProps, Q>;
interface NextStatelessComponent<TProps = {}, Q = DefaultQuery>
extends React.StatelessComponent<TProps> {
getInitialProps?: (ctx: NextContext<Q>) => Promise<TProps>;
}
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<void>;
defineRoutes(): Promise<void>;
start(): Promise<void>;
run(
req: http.IncomingMessage,
res: http.ServerResponse,
parsedUrl: UrlLike
): Promise<void>;
run(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: UrlLike): Promise<void>;
render(
req: http.IncomingMessage,
@@ -182,6 +176,49 @@ declare namespace next {
getCompilationError(): Promise<any>;
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<P = {}, IP = P, C = NextContext> =
| NextComponentClass<P, IP, C>
| NextStatelessComponent<P, IP, C>;
/**
* 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<P = {}, IP = P, C = NextContext> = NextStatelessComponent<P, IP, C>;
type NextStatelessComponent<P = {}, IP = P, C = NextContext> = React.StatelessComponent<P> &
NextStaticLifecycle<IP, C>;
/**
* 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<P = {}, IP = P, C = NextContext> = React.ComponentClass<P> &
NextStaticLifecycle<IP, C>;
/**
* 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<IP, C> {
getInitialProps?: (ctx: C) => Promise<IP> | IP;
}
}
declare function next(options?: next.ServerOptions): next.Server;

View File

@@ -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<NextComponentProps> {
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<NextComponentProps> {
);
}
}
class TestAppWithProps extends App<NextComponentProps> {
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 <Component {...pageProps} example={example} />;
}
}
class TestAppWithTypedQuery extends App<{}, NextAppContext<TypedQuery>> {
static async getInitialProps({ ctx }: NextAppContext<TypedQuery>) {
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 = <P extends {}>(App: AppComponentType<P & WithExampleProps>) =>
class extends React.Component<P & AppProps & WithExampleHocProps> {
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 <App {...this.props} example={this.test} />;
}
};
// Basic stateless HOC. Similar to what withAuth would do.
// tslint:disable-next-line no-unnecessary-generics
const withBasic = <P extends {}, C extends {}>(App: AppComponentType<P, C>) =>
class extends React.Component<P & AppProps> {
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 <App {...this.props} />;
}
};
withExample(TestAppWithProps);
withBasic(TestAppWithTypedQuery);

View File

@@ -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<NextComponentProps> {
static async getInitialProps(ctx: NextContext) {
const { example } = ctx.query;
return { example };
return { example: example as string };
}
render() {
return (
<div>I'm a class component! {this.props.example}</div>
);
return <div>I'm a class component! {this.props.example}</div>;
}
}
@@ -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 = <P extends {}>(Page: NextComponentType<P & WithExampleProps, P>) =>
class extends React.Component<P & WithExampleHocProps> {
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 <Page {...this.props} example={this.test} />;
}
};
// Basic stateless HOC. Similar to what withAuth would do.
// tslint:disable-next-line no-unnecessary-generics
const withBasic = <P extends {}>(Page: NextComponentType<P>) =>
class extends React.Component<P> {
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 <Page {...this.props} />;
}
};
class NextWithExample extends React.Component<TestProps & WithExampleProps> {
static async getInitialProps(ctx: NextContext<TypedQuery>) {
const { id } = ctx.query;
const processQuery = (id?: string) => id;
processQuery(id);
return { ownProp: true };
}
render() {
const { ownProp, example } = this.props;
return (
<div>
I'm wrapped in a HOC that gives me an example prop! {example} {ownProp}
</div>
);
}
}
// P template is inferred as <TestProps>.
withExample(NextWithExample);
// P template inferred as <NextComponentProps>.
withBasic(ClassNext);
// P template inferred as <TestProps & WithExampleProps>
withBasic(withExample(NextWithExample));

View File

@@ -1,3 +1,3 @@
import withTypescript = require('@zeit/next-typescript');
withTypescript({}); // $ExpectType ServerConfig
withTypescript({}); // $ExpectType NextConfig