Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

increased strictness of types #695

Merged
merged 16 commits into from
May 30, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Expect active development and potentially significant breaking changes in the `0

### vNext

### 1.4.0
- Feature: Enhanced typescript definitions to allow for more valid type checking of graphql HOC
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will be a breaking change for TS devs, right? Might be worth a mention in the changelog.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stubailo for sure! I'll add that and note it in the release as well


### 1.3.0
- Feature: Support tree shaking and smaller (marginally) bundles via rollup [PR #691](https://github.com/apollographql/react-apollo/pull/691)
- Fix: Render full markup on the server when using the `cache-and-network` fetchPolicy [PR #688](https://github.com/apollographql/react-apollo/pull/688)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-apollo",
"version": "1.3.0",
"version": "1.4.0",
"description": "React data container for Apollo Client",
"main": "lib/react-apollo.umd.js",
"module": "./lib/index.js",
Expand Down
2 changes: 1 addition & 1 deletion src/browser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { default as ApolloProvider } from './ApolloProvider';
export { default as graphql, InjectedGraphQLProps } from './graphql';
export { default as graphql } from './graphql';
export { withApollo } from './withApollo';

// expose easy way to join queries from redux
Expand Down
67 changes: 39 additions & 28 deletions src/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import ApolloClient, {
} from 'apollo-client';

import {
// GraphQLResult,
ExecutionResult,
DocumentNode,
} from 'graphql';

Expand All @@ -50,11 +50,11 @@ export declare interface QueryOptions {
skip?: boolean;
}

export interface GraphQLDataProps {
export interface QueryProps {
error?: ApolloError;
networkStatus: number;
loading: boolean;
variables: {
variables?: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this should not have the ? it looks like if there's no variables it's an empty object, not undefined

[variable: string]: any;
};
fetchMore: (fetchMoreOptions: FetchMoreQueryOptions & FetchMoreOptions) => Promise<ApolloQueryResult<any>>;
Expand All @@ -65,8 +65,33 @@ export interface GraphQLDataProps {
updateQuery: (mapFn: (previousQueryResult: any, options: UpdateQueryOptions) => any) => void;
}

export interface InjectedGraphQLProps<T> {
data?: T & GraphQLDataProps;
export type MutationFunc<TResult> = (opts: MutationOptions) => Promise<ApolloQueryResult<TResult>>;

export interface OptionProps<TProps, TResult> {
ownProps: TProps;
data?: QueryProps & TResult;
mutate?: MutationFunc<TResult>;
}

export type DefaultChildProps<P, R> = P & { data?: QueryProps & R, mutate?: MutationFunc<R> };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow this is dope

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right? I'm wondering if I setup some overloads if we can get it to correctly type mutation / query / subscription. Going to try my hand at it tonight


export interface OperationOption<TProps, TResult> {
options?: QueryOptions | MutationOptions | ((props: TProps) => QueryOptions | MutationOptions);
props?: (props: OptionProps<TProps, TResult>) => any;
skip?: boolean | ((props: any) => boolean);
name?: string;
withRef?: boolean;
shouldResubscribe?: (props: TProps, nextProps: TProps) => boolean;
Copy link

@ianks ianks May 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would props and nextProps not be optional here?

i.e.

{
  //...
  shouldSubscribe: () => true //should be OK
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that should still be fine, they should only be optional if the call can pass the variables as undefined

interface Test {
  testFn (x: string, y: string): boolean;
}

let z: Test = { 
  // this is fine
  testFn: () => true
};

alias?: string;
}

export type CompositeComponent<P> = ComponentClass<P> | StatelessComponent<P>;

export interface ComponentDecorator<TOwnProps, TMergedProps> {
(component: CompositeComponent<TMergedProps>): ComponentClass<TOwnProps>;
}
export interface InferableComponentDecorator<TOwnProps> {
<T extends CompositeComponent<TOwnProps>>(component: T): T;
}

const defaultMapPropsToOptions = props => ({});
Expand Down Expand Up @@ -94,24 +119,10 @@ function getDisplayName(WrappedComponent) {
// Helps track hot reloading.
let nextVersion = 0;

export interface OperationOption {
options?: Object | ((props: any) => QueryOptions | MutationOptions);
props?: (props: any) => any;
skip?: boolean | ((props: any) => boolean);
name?: string;
withRef?: boolean;
shouldResubscribe?: (props: any, nextProps: any) => boolean;
alias?: string;
}

export interface WrapWithApollo {
<P, TComponentConstruct extends (ComponentClass<P> | StatelessComponent<P>)>(component: TComponentConstruct): TComponentConstruct;
}

export default function graphql(
export default function graphql<TResult = {}, TProps = {}, TChildProps = DefaultChildProps<TProps, TResult>>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think you'll need to bump typescript to 2.3 in package.json for the default generics

document: DocumentNode,
operationOptions: OperationOption = {},
) {
operationOptions: OperationOption<TProps, TResult> = {},
): ComponentDecorator<TProps, TChildProps> {

// extract options
const { options = defaultMapPropsToOptions, skip = defaultMapPropsToSkip, alias = 'Apollo' } = operationOptions;
Expand All @@ -130,7 +141,7 @@ export default function graphql(
// Helps track hot reloading.
const version = nextVersion++;

const wrapWithApolloComponent: WrapWithApollo = WrappedComponent => {
function wrapWithApolloComponent(WrappedComponent) {

const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`;

Expand All @@ -142,7 +153,7 @@ export default function graphql(
// However, this is an unlikely scenario.
const recycler = new ObservableQueryRecycler();

class GraphQL extends Component<any, any> {
class GraphQL extends Component<TProps, any> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not important, but can probably set state to {}

static displayName = graphQLDisplayName;
static WrappedComponent = WrappedComponent;
static contextTypes = {
Expand Down Expand Up @@ -297,11 +308,11 @@ export default function graphql(
return opts;
}

calculateResultProps(result) {
calculateResultProps(result: (QueryProps & TResult) | MutationFunc<TResult>) {
let name = this.type === DocumentType.Mutation ? 'mutate' : 'data';
if (operationOptions.name) name = operationOptions.name;

const newResult = { [name]: result, ownProps: this.props };
const newResult: OptionProps<TProps, TResult> = { [name]: result, ownProps: this.props };
if (mapResultToProps) return mapResultToProps(newResult);

return { [name]: defaultMapResultToProps(result) };
Expand Down Expand Up @@ -520,7 +531,7 @@ export default function graphql(
this.previousData = currentResult.data;
}
}
return data;
return (data as QueryProps & TResult);
}

render() {
Expand All @@ -547,7 +558,7 @@ export default function graphql(

// Make sure we preserve any custom statics on the original component.
return hoistNonReactStatics(GraphQL, WrappedComponent, {});
};
}

return wrapWithApolloComponent;
}
Expand Down
6 changes: 2 additions & 4 deletions src/withApollo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,15 @@ import {
OperationOption,
MutationOptions,
QueryOptions,
GraphQLDataProps,
InjectedGraphQLProps,
} from './graphql';

function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

export function withApollo(
export function withApollo<TProps, TResult>(
WrappedComponent,
operationOptions: OperationOption = {},
operationOptions: OperationOption<TProps, TResult> = {},
) {

const withDisplayName = `withApollo(${getDisplayName(WrappedComponent)})`;
Expand Down
11 changes: 9 additions & 2 deletions test/react-web/client/graphql/queries/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ declare function require(name: string);

import { mockNetworkInterface } from '../../../../../src/test-utils';
import { ApolloProvider, graphql} from '../../../../../src';
import { DocumentType } from '../../../../../src/parser';

// XXX: this is also defined in apollo-client
// I'm not sure why mocha doesn't provide something like this, you can't
Expand All @@ -40,9 +41,15 @@ describe('queries', () => {
const networkInterface = mockNetworkInterface({ request: { query }, result: { data } });
const client = new ApolloClient({ networkInterface, addTypename: false });

const ContainerWithData = graphql(query)(({ data }) => { // tslint:disable-line
type ResultData = {
allPeople?: {
people: { name: string }[]
}
}


const ContainerWithData = graphql<ResultData>(query)(({ data }) => { // tslint:disable-line
expect(data).toBeTruthy();
expect(data.ownProps).toBeFalsy();
expect(data.loading).toBe(true);
return null;
});
Expand Down
43 changes: 36 additions & 7 deletions test/react-web/client/graphql/queries/reducer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,15 @@ describe('[queries] reducer', () => {
const networkInterface = mockNetworkInterface({ request: { query }, result: { data } });
const client = new ApolloClient({ networkInterface, addTypename: false });

const props = ({ data }) => ({ showSpinner: data.loading });
const ContainerWithData = graphql(query, { props })(({ showSpinner }) => {
type ResultData = {
getThing: { thing: boolean }
}
type ResultShape = {
showSpinner: boolean
}

const props = (result) => ({ showSpinner: result.data && result.data.loading });
const ContainerWithData = graphql<ResultData, {}, ResultShape>(query, { props })(({ showSpinner }) => {
expect(showSpinner).toBe(true);
return null;
});
Expand All @@ -60,7 +67,16 @@ describe('[queries] reducer', () => {
expect(ownProps.sample).toBe(1);
return { showSpinner: data.loading };
};
const ContainerWithData = graphql(query, { props })(({ showSpinner }) => {
type ResultData = {
getThing: { thing: boolean }
}
type ReducerResult = {
showSpinner: boolean,
}
type Props = {
sample: number
}
const ContainerWithData = graphql<ResultData, Props, ReducerResult>(query, { props })(({ showSpinner }) => {
expect(showSpinner).toBe(true);
return null;
});
Expand All @@ -77,9 +93,20 @@ describe('[queries] reducer', () => {
const networkInterface = mockNetworkInterface({ request: { query }, result: { data } });
const client = new ApolloClient({ networkInterface, addTypename: false });

@graphql(query, { props: ({ data }) => ({ thingy: data.getThing }) }) // tslint:disable-line
class Container extends React.Component<any, any> {
componentWillReceiveProps(props) {
type Result = {
getThing?: { thing: boolean }
}

type PropsResult = {
thingy: boolean
}

const withData = graphql<Result, {}, PropsResult>(query, {
props: ({ data }) => ({ thingy: data.getThing })
})

class Container extends React.Component<PropsResult, any> {
componentWillReceiveProps(props: PropsResult) {
expect(props.thingy).toEqual(data.getThing);
done();
}
Expand All @@ -88,7 +115,9 @@ describe('[queries] reducer', () => {
}
};

renderer.create(<ApolloProvider client={client}><Container /></ApolloProvider>);
const ContainerWithData = withData(Container);

renderer.create(<ApolloProvider client={client}><ContainerWithData /></ApolloProvider>);
});


Expand Down