From 575f2060e88587da3bc26c1755206a91d02d12ab Mon Sep 17 00:00:00 2001 From: Drew Atkinson Date: Fri, 20 Sep 2024 10:48:56 -0700 Subject: [PATCH] Add satisfies type assertion for typescript (#4797) Summary: Fixes https://github.com/facebook/relay/issues/4772 Adds a satisfies assertion to the live resolver import: ``` import {User as userRelayModelInstanceResolverType} from "UserTypeResolvers"; // Type assertion validating that `userRelayModelInstanceResolverType` resolver is correctly implemented. // A type error here indicates that the type signature of the resolver module is incorrect. (userRelayModelInstanceResolverType satisfies ( id: User__id$data['id'], args: void, ) => LiveState); ``` It will also require a minimum Typescript version of TS 4.9 or higher for consumers, which can be checked as we implement https://github.com/facebook/relay/issues/4755. We may have to have add a feature flag to disable this for consumers of the library which do not meet this minimum standard. Pull Request resolved: https://github.com/facebook/relay/pull/4797 Reviewed By: tyao1 Differential Revision: D63032995 Pulled By: captbaritone fbshipit-source-id: be909c70f38312cd5931d7824b2e887b7834dc1c --- .../typescript_resolver_type_import.expected | 5 +- .../typescript_resolver_with_context.expected | 10 +++- .../crates/relay-typegen/src/typescript.rs | 47 +++++++++++++++++-- compiler/crates/relay-typegen/src/visit.rs | 8 +--- ...with-output-type-client-interface.expected | 5 ++ ...er-with-output-type-client-object.expected | 5 ++ ...ype-relay-resolver-value-required.expected | 5 ++ ...-output-type-relay-resolver-value.expected | 5 ++ ...tic_non_null_liked_field_resolver.expected | 5 ++ ...on_null_liked_field_weak_resolver.expected | 3 ++ ...semantic_non_null_scalar_resolver.expected | 3 ++ 11 files changed, 86 insertions(+), 15 deletions(-) diff --git a/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/typescript_resolver_type_import.expected b/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/typescript_resolver_type_import.expected index ce9b7fbc62a57..443e1581f1998 100644 --- a/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/typescript_resolver_type_import.expected +++ b/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/typescript_resolver_type_import.expected @@ -25,7 +25,7 @@ type User { name: String } ==================================== OUTPUT =================================== //- __generated__/barFragment.graphql.ts /** - * SignedSource<> + * SignedSource<<0f778584db7b20b3491a3ed42c61cdc1>> * @lightSyntaxTransform * @nogrep */ @@ -37,6 +37,9 @@ type User { name: String } import { ReaderFragment } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; import { foo as userFooResolverType } from "../foo"; +// Type assertion validating that `userFooResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(userFooResolverType satisfies () => unknown | null | undefined); export type barFragment$data = { readonly foo: ReturnType | null | undefined; readonly " $fragmentType": "barFragment"; diff --git a/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/typescript_resolver_with_context.expected b/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/typescript_resolver_with_context.expected index 70638a8f35fdb..dd01db7cb2e0d 100644 --- a/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/typescript_resolver_with_context.expected +++ b/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/typescript_resolver_with_context.expected @@ -28,7 +28,7 @@ type User { name: String } ==================================== OUTPUT =================================== //- __generated__/barFragment.graphql.ts /** - * SignedSource<<5dff27df4f02d14a44779d79d3f98fbb>> + * SignedSource<<7c2c6939d2e5610ed47a154e9cab2dfd>> * @lightSyntaxTransform * @nogrep */ @@ -38,9 +38,15 @@ type User { name: String } // @ts-nocheck import { ReaderFragment } from 'relay-runtime'; -import { FragmentRefs } from "relay-runtime"; +import { LiveState, FragmentRefs } from "relay-runtime"; import { foo as userFooResolverType } from "../foo"; import { ITestResolverContextType } from "@test/package"; +// Type assertion validating that `userFooResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(userFooResolverType satisfies ( + args: undefined, + context: ITestResolverContextType, +) => LiveState); export type barFragment$data = { readonly foo: ReturnType["read"]> | null | undefined; readonly " $fragmentType": "barFragment"; diff --git a/compiler/crates/relay-typegen/src/typescript.rs b/compiler/crates/relay-typegen/src/typescript.rs index d7d77118ae081..0410b68276fa4 100644 --- a/compiler/crates/relay-typegen/src/typescript.rs +++ b/compiler/crates/relay-typegen/src/typescript.rs @@ -13,6 +13,8 @@ use intern::intern; use itertools::Itertools; use relay_config::TypegenConfig; +use crate::writer::FunctionTypeAssertion; +use crate::writer::KeyValuePairProp; use crate::writer::Prop; use crate::writer::SortedASTList; use crate::writer::SortedStringKeyList; @@ -77,11 +79,11 @@ impl Writer for TypeScriptPrinter { AST::ReturnTypeOfMethodCall(object, method_name) => { self.write_return_type_of_method_call(object, *method_name) } - AST::AssertFunctionType(_) => { - // TODO: Add proper support for Resolver type generation in - // typescript: https://github.com/facebook/relay/issues/4772 - Ok(()) - } + AST::AssertFunctionType(FunctionTypeAssertion { + function_name, + arguments, + return_type, + }) => self.write_assert_function_type(*function_name, arguments, return_type), AST::GenericType { outer, inner } => self.write_generic_type(*outer, inner), AST::PropertyType { type_, @@ -364,6 +366,41 @@ impl TypeScriptPrinter { } write!(&mut self.result, ">") } + + fn write_assert_function_type( + &mut self, + function_name: StringKey, + arguments: &[KeyValuePairProp], + return_type: &AST, + ) -> FmtResult { + writeln!( + &mut self.result, + "// Type assertion validating that `{}` resolver is correctly implemented.", + function_name + )?; + writeln!( + &mut self.result, + "// A type error here indicates that the type signature of the resolver module is incorrect." + )?; + if arguments.is_empty() { + write!(&mut self.result, "({} satisfies (", function_name)?; + } else { + writeln!(&mut self.result, "({} satisfies (", function_name)?; + self.indentation += 1; + for argument in arguments.iter() { + self.write_indentation()?; + write!(&mut self.result, "{}: ", argument.key)?; + self.write(&argument.value)?; + writeln!(&mut self.result, ",")?; + } + self.indentation -= 1; + } + write!(&mut self.result, ") => ")?; + self.write(return_type)?; + writeln!(&mut self.result, ");")?; + + Ok(()) + } } #[cfg(test)] diff --git a/compiler/crates/relay-typegen/src/visit.rs b/compiler/crates/relay-typegen/src/visit.rs index 6abe2a6eec081..765f149da8f17 100644 --- a/compiler/crates/relay-typegen/src/visit.rs +++ b/compiler/crates/relay-typegen/src/visit.rs @@ -402,13 +402,7 @@ fn generate_resolver_type( let ast = transform_type_reference_into_ast(&schema_field_type, |_| inner_ast); - let return_type = if matches!( - typegen_context.project_config.typegen_config.language, - TypegenLanguage::TypeScript - ) { - // TODO: Add proper support for Resolver type generation in typescript: https://github.com/facebook/relay/issues/4772 - AST::Any - } else if resolver_metadata.live { + let return_type = if resolver_metadata.live { runtime_imports.resolver_live_state_type = true; AST::GenericType { outer: *LIVE_STATE_TYPE, diff --git a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-client-interface.expected b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-client-interface.expected index 0e483e7a93213..6b0824de8d315 100644 --- a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-client-interface.expected +++ b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-client-interface.expected @@ -69,6 +69,11 @@ export type User__pop_star_name$normalization = { ------------------------------------------------------------------------------- import type { FragmentRefs } from "relay-runtime"; import userPopStarNameResolverType from "PopStarNameResolver"; +// Type assertion validating that `userPopStarNameResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(userPopStarNameResolverType satisfies ( + rootKey: PopStarNameResolverFragment_name$key, +) => User__pop_star_name$normalization | null | undefined); export type Foo_user$data = { readonly poppy: { readonly name: string | null | undefined; diff --git a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-client-object.expected b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-client-object.expected index 8ced180aaa4a6..156591332c0e7 100644 --- a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-client-object.expected +++ b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-client-object.expected @@ -38,6 +38,11 @@ export type User__pop_star_name$normalization = { ------------------------------------------------------------------------------- import type { FragmentRefs } from "relay-runtime"; import userPopStarNameResolverType from "PopStarNameResolver"; +// Type assertion validating that `userPopStarNameResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(userPopStarNameResolverType satisfies ( + rootKey: PopStarNameResolverFragment_name$key, +) => User__pop_star_name$normalization | null | undefined); export type Foo_user$data = { readonly poppy: { readonly name: string | null | undefined; diff --git a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-relay-resolver-value-required.expected b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-relay-resolver-value-required.expected index b50a69212fe9a..c67cc64761452 100644 --- a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-relay-resolver-value-required.expected +++ b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-relay-resolver-value-required.expected @@ -21,6 +21,11 @@ extend type User { ==================================== OUTPUT =================================== import { FragmentRefs } from "relay-runtime"; import userPopStarNameResolverType from "PopStarNameResolver"; +// Type assertion validating that `userPopStarNameResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(userPopStarNameResolverType satisfies ( + rootKey: PopStarNameResolverFragment_name$key, +) => unknown | null | undefined); export type Foo_user$data = { readonly poppy: NonNullable>; readonly " $fragmentType": "Foo_user"; diff --git a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-relay-resolver-value.expected b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-relay-resolver-value.expected index 68d4f2d58dff7..fc7174d94a82c 100644 --- a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-relay-resolver-value.expected +++ b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/relay-resolver-with-output-type-relay-resolver-value.expected @@ -21,6 +21,11 @@ extend type User { ==================================== OUTPUT =================================== import { FragmentRefs } from "relay-runtime"; import userPopStarNameResolverType from "PopStarNameResolver"; +// Type assertion validating that `userPopStarNameResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(userPopStarNameResolverType satisfies ( + rootKey: PopStarNameResolverFragment_name$key, +) => unknown | null | undefined); export type Foo_user$data = { readonly poppy: ReturnType | null | undefined; readonly " $fragmentType": "Foo_user"; diff --git a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_liked_field_resolver.expected b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_liked_field_resolver.expected index 61ac8b7e4e218..b2d579a3aa371 100644 --- a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_liked_field_resolver.expected +++ b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_liked_field_resolver.expected @@ -29,6 +29,11 @@ export type ClientEdgeQuery_MyFragment_best_friend = { ------------------------------------------------------------------------------- import { FragmentRefs, DataID } from "relay-runtime"; import clientUserBestFriendResolverType from "bar"; +// Type assertion validating that `clientUserBestFriendResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(clientUserBestFriendResolverType satisfies () => { + readonly id: DataID; +}); export type MyFragment$data = { readonly best_friend: { readonly name: string | null | undefined; diff --git a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_liked_field_weak_resolver.expected b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_liked_field_weak_resolver.expected index 7a31b1a3de38c..60c20e53d3286 100644 --- a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_liked_field_weak_resolver.expected +++ b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_liked_field_weak_resolver.expected @@ -29,6 +29,9 @@ export type ClientUser__blob$normalization = { ------------------------------------------------------------------------------- import { FragmentRefs } from "relay-runtime"; import clientUserBlobResolverType from "bar"; +// Type assertion validating that `clientUserBlobResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(clientUserBlobResolverType satisfies () => ClientUser__blob$normalization); export type MyFragment$data = { readonly blob: { readonly data: string | null | undefined; diff --git a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_scalar_resolver.expected b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_scalar_resolver.expected index c8b891b5a68b5..7771a21e8959d 100644 --- a/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_scalar_resolver.expected +++ b/compiler/crates/relay-typegen/tests/generate_typescript/fixtures/semantic_non_null_scalar_resolver.expected @@ -13,6 +13,9 @@ type ClientUser { ==================================== OUTPUT =================================== import { FragmentRefs } from "relay-runtime"; import clientUserNameResolverType from "bar"; +// Type assertion validating that `clientUserNameResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(clientUserNameResolverType satisfies () => unknown); export type MyFragment$data = { readonly name: NonNullable>; readonly " $fragmentType": "MyFragment";