From de6594bf3e8443268662e45cf7a704fdab541e87 Mon Sep 17 00:00:00 2001 From: Sam Zhou Date: Tue, 30 May 2023 21:59:08 -0700 Subject: [PATCH] Using opaque type trick to enforce nested relay entry point safety Reviewed By: josephsavona Differential Revision: D45799755 fbshipit-source-id: c2e49731319e1250746e7cb858d49f56d9993ae7 --- .../relay-hooks/EntryPointTypes.flow.js | 22 ++++---- .../NestedRelayEntryPointBuilderUtils.js | 51 +++++++++++++++++++ .../NestedEntrypoints-flowtest.js | 12 +++-- 3 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 packages/react-relay/relay-hooks/NestedRelayEntryPointBuilderUtils.js diff --git a/packages/react-relay/relay-hooks/EntryPointTypes.flow.js b/packages/react-relay/relay-hooks/EntryPointTypes.flow.js index 90e92f4464604..b9e1b667b8ebb 100644 --- a/packages/react-relay/relay-hooks/EntryPointTypes.flow.js +++ b/packages/react-relay/relay-hooks/EntryPointTypes.flow.js @@ -131,7 +131,7 @@ defined during component runtime TExtraProps - a bag of extra props that you may define in `entrypoint` file and they will be passed to the EntryPointComponent as `extraProps` */ -type InternalEntryPointRepresentation< +export type InternalEntryPointRepresentation< TEntryPointParams, TPreloadedQueries, TPreloadedEntryPoints, @@ -238,24 +238,20 @@ export type ThinQueryParams< variables: TQuery['variables'], }>; -type ThinNestedEntryPointParams = $ReadOnly<{ - entryPoint: TEntryPoint, - entryPointParams: TEntryPointParams, -}>; +/** + * We make the type of `ThinNestedEntryPointParams` opaque, so that the only way + * to construct a `ThinNestedEntryPointParams` is by calling `NestedRelayEntryPoint` + * from `NestedRelayEntryPointBuilderUtils` module. + */ +declare export opaque type ThinNestedEntryPointParams; export type ExtractQueryTypeHelper = ( PreloadedQuery, ) => ThinQueryParams; -export type ExtractEntryPointTypeHelper = < - TEntryPointParams, - TEntryPointComponent, ->( +export type ExtractEntryPointTypeHelper = ( ?PreloadedEntryPoint, -) => ?ThinNestedEntryPointParams< - TEntryPointParams, - EntryPoint, ->; +) => ?ThinNestedEntryPointParams; export type EntryPoint = InternalEntryPointRepresentation< diff --git a/packages/react-relay/relay-hooks/NestedRelayEntryPointBuilderUtils.js b/packages/react-relay/relay-hooks/NestedRelayEntryPointBuilderUtils.js new file mode 100644 index 0000000000000..1f2e7bea51f94 --- /dev/null +++ b/packages/react-relay/relay-hooks/NestedRelayEntryPointBuilderUtils.js @@ -0,0 +1,51 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall relay + */ + +'use strict'; + +import type { + InternalEntryPointRepresentation, + ThinNestedEntryPointParams, +} from './EntryPointTypes.flow'; + +/** + * This is an identity function to construct a type safe nested entrypoint. + * By calling this function, we ensure that the type of entryPointParams matches + * exactly the type of preloadProps of the entrypoint. + * + * We make the type of `ThinNestedEntryPointParams` opaque, so that the only way + * to construct a `ThinNestedEntryPointParams` is by calling this function. + */ +declare function NestedRelayEntryPoint< + TEntryPointParams, + TPreloadedQueries, + TPreloadedEntryPoints, + TRuntimeProps, + TExtraProps, +>( + $ReadOnly<{ + entryPoint: InternalEntryPointRepresentation< + TEntryPointParams, + TPreloadedQueries, + TPreloadedEntryPoints, + TRuntimeProps, + TExtraProps, + >, + entryPointParams: TEntryPointParams, + }>, +): ThinNestedEntryPointParams; + +// eslint-disable-next-line no-redeclare +function NestedRelayEntryPoint

(params: P): P { + return params; +} + +export {NestedRelayEntryPoint}; diff --git a/packages/react-relay/relay-hooks/__flowtests__/EntryPointTypes/NestedEntrypoints-flowtest.js b/packages/react-relay/relay-hooks/__flowtests__/EntryPointTypes/NestedEntrypoints-flowtest.js index c722ac27970f5..849593768a9f1 100644 --- a/packages/react-relay/relay-hooks/__flowtests__/EntryPointTypes/NestedEntrypoints-flowtest.js +++ b/packages/react-relay/relay-hooks/__flowtests__/EntryPointTypes/NestedEntrypoints-flowtest.js @@ -18,6 +18,8 @@ import type { } from '../../EntryPointTypes.flow'; import type {JSResourceReference} from 'JSResourceReference'; +import {NestedRelayEntryPoint} from '../../NestedRelayEntryPointBuilderUtils'; + declare function mockJSResource( module: TModule, ): JSResourceReference; @@ -65,14 +67,14 @@ type BadParentEntrypointParams = $ReadOnly<{}>; getPreloadProps(_params: BadParentEntrypointParams) { return { entryPoints: { - nestedComponent: { - entryPoint: NestedEntryPoint, + nestedComponent: NestedRelayEntryPoint({ /** $FlowExpectedError The entryPointParams here should be of type NestedEntrypointPreloadParams, but it does not contain subEntrypointPreloadParam */ + entryPoint: NestedEntryPoint, entryPointParams: Object.freeze({}), - }, + }), }, }; }, @@ -90,13 +92,13 @@ type GoodParentEntrypointParams = $ReadOnly<{}>; getPreloadProps(_params: GoodParentEntrypointParams) { return { entryPoints: { - nestedComponent: { + nestedComponent: NestedRelayEntryPoint({ entryPoint: NestedEntryPoint, // No flow error since this matches NestedEntrypointPreloadParams entryPointParams: { subEntrypointPreloadParam: 'test', }, - }, + }), }, }; },