Skip to content

Commit cc47bc9

Browse files
tobias-tenglerfacebook-github-bot
authored andcommitted
Fix type generation of nullable types in TypeScript (#4380)
Summary: Closes: #4379 ## Release Notes Previously, the relay-compiler would generate a union of the actual type and `null` (`T | null`) for nullable fields when targeting TypeScript. This was not correct, since the Relay store can also return `undefined` in the case of missing data. This version now produces a union of the actual type, `null` and `undefined` (`T | null | undefined`). Consequently you now also have to check both the `null` and `undefined` case, before accessing a field's value: ```ts const data = useLazyLoadQuery(/* ... */); // Notice the `!=` instead of `!==` to check against null and undefined if (data.nullableField != null) { // Safely access data.nullableField } ``` Since this is a pretty big change, we're offering the `typescriptExcludeUndefinedFromNullableUnion` feature flag in the `relay-compiler` config to keep the old type generation behavior for now: ```json { "language": "typescript", "typescriptExcludeUndefinedFromNullableUnion": true } ``` Just be aware that we will remove this feature flag in a future release and that you might run into an unexpected `undefined`, if you're not checking for `undefined` before accessing a field - especially if it's a client-only field. Pull Request resolved: #4380 Reviewed By: alunyov Differential Revision: D48989476 Pulled By: captbaritone fbshipit-source-id: 52c5f215298de50e8506245a9d6696e1e935fc2a
1 parent 9eb2747 commit cc47bc9

File tree

55 files changed

+457
-440
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+457
-440
lines changed

compiler/crates/relay-config/src/typegen_config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ pub struct TypegenConfig {
108108
/// This option enables emitting es modules artifacts.
109109
#[serde(default)]
110110
pub eager_es_modules: bool,
111+
112+
/// Keep the previous compiler behavior by outputting an union
113+
/// of the raw type and null, and not the **correct** behavior
114+
/// of an union with the raw type, null and undefined.
115+
#[serde(default)]
116+
pub typescript_exclude_undefined_from_nullable_union: bool,
111117
}
112118

113119
#[derive(Default, Debug, Serialize, Deserialize, Clone, Copy)]

compiler/crates/relay-typegen/src/typescript.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::KEY_FRAGMENT_TYPE;
2626
pub struct TypeScriptPrinter {
2727
result: String,
2828
use_import_type_syntax: bool,
29+
include_undefined_in_nullable_union: bool,
2930
indentation: usize,
3031
}
3132

@@ -170,6 +171,8 @@ impl TypeScriptPrinter {
170171
result: String::new(),
171172
indentation: 0,
172173
use_import_type_syntax: config.use_import_type_syntax,
174+
include_undefined_in_nullable_union: !config
175+
.typescript_exclude_undefined_from_nullable_union,
173176
}
174177
}
175178

@@ -212,13 +215,21 @@ impl TypeScriptPrinter {
212215

213216
fn write_nullable(&mut self, of_type: &AST) -> FmtResult {
214217
let null_type = AST::RawType("null".intern());
218+
let undefined_type = AST::RawType("undefined".intern());
215219
if let AST::Union(members) = of_type {
216220
let mut new_members = Vec::with_capacity(members.len() + 1);
217221
new_members.extend_from_slice(members);
218222
new_members.push(null_type);
223+
if self.include_undefined_in_nullable_union {
224+
new_members.push(undefined_type);
225+
}
219226
self.write_union(&*new_members)?;
220227
} else {
221-
self.write_union(&*vec![of_type.clone(), null_type])?;
228+
let mut union_members = vec![of_type.clone(), null_type];
229+
if self.include_undefined_in_nullable_union {
230+
union_members.push(undefined_type)
231+
}
232+
self.write_union(&*union_members)?;
222233
}
223234
Ok(())
224235
}
@@ -387,14 +398,14 @@ mod tests {
387398
fn nullable_type() {
388399
assert_eq!(
389400
print_type(&AST::Nullable(Box::new(AST::String))),
390-
"string | null".to_string()
401+
"string | null | undefined".to_string()
391402
);
392403

393404
assert_eq!(
394405
print_type(&AST::Nullable(Box::new(AST::Union(SortedASTList::new(
395406
vec![AST::String, AST::Number],
396407
))))),
397-
"string | number | null"
408+
"string | number | null | undefined"
398409
)
399410
}
400411

compiler/crates/relay-typegen/tests/generate_typescript/fixtures/aliased-fragment-raw-response-type.expected

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@ export type MyQuery$variables = {};
1717
export type MyQuery$data = {
1818
readonly me: {
1919
readonly my_inline_fragment: {
20-
readonly name: string | null;
21-
} | null;
20+
readonly name: string | null | undefined;
21+
} | null | undefined;
2222
readonly my_user: {
2323
readonly " $fragmentSpreads": FragmentRefs<"MyUserFragment">;
24-
} | null;
25-
} | null;
24+
} | null | undefined;
25+
} | null | undefined;
2626
};
2727
export type MyQuery$rawResponse = {
2828
readonly me: {
2929
readonly id: string;
30-
readonly name: string | null;
31-
} | null;
30+
readonly name: string | null | undefined;
31+
} | null | undefined;
3232
};
3333
export type MyQuery = {
3434
rawResponse: MyQuery$rawResponse;
@@ -38,7 +38,7 @@ export type MyQuery = {
3838
-------------------------------------------------------------------------------
3939
import { FragmentRefs } from "relay-runtime";
4040
export type MyUserFragment$data = {
41-
readonly name: string | null;
41+
readonly name: string | null | undefined;
4242
readonly " $fragmentType": "MyUserFragment";
4343
};
4444
export type MyUserFragment$key = {

compiler/crates/relay-typegen/tests/generate_typescript/fixtures/aliased-fragment-spread-in-abstract-selection.expected

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export type RelayReaderNamedFragmentsTest2Query$data = {
1515
readonly node: {
1616
readonly named_fragment: {
1717
readonly " $fragmentSpreads": FragmentRefs<"RelayReaderNamedFragmentsTest_maybe_node_interface">;
18-
} | null;
19-
} | null;
18+
} | null | undefined;
19+
} | null | undefined;
2020
};
2121
export type RelayReaderNamedFragmentsTest2Query = {
2222
response: RelayReaderNamedFragmentsTest2Query$data;
@@ -25,7 +25,7 @@ export type RelayReaderNamedFragmentsTest2Query = {
2525
-------------------------------------------------------------------------------
2626
import { FragmentRefs } from "relay-runtime";
2727
export type RelayReaderNamedFragmentsTest_maybe_node_interface$data = {
28-
readonly name: string | null;
28+
readonly name: string | null | undefined;
2929
readonly " $fragmentType": "RelayReaderNamedFragmentsTest_maybe_node_interface";
3030
};
3131
export type RelayReaderNamedFragmentsTest_maybe_node_interface$key = {

compiler/crates/relay-typegen/tests/generate_typescript/fixtures/aliased-fragment-spread.expected

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export type RelayReaderNamedFragmentsTest2Query$data = {
1515
readonly me: {
1616
readonly named_fragment: {
1717
readonly " $fragmentSpreads": FragmentRefs<"RelayReaderNamedFragmentsTest_user">;
18-
} | null;
19-
} | null;
18+
} | null | undefined;
19+
} | null | undefined;
2020
};
2121
export type RelayReaderNamedFragmentsTest2Query = {
2222
response: RelayReaderNamedFragmentsTest2Query$data;
@@ -25,7 +25,7 @@ export type RelayReaderNamedFragmentsTest2Query = {
2525
-------------------------------------------------------------------------------
2626
import { FragmentRefs } from "relay-runtime";
2727
export type RelayReaderNamedFragmentsTest_user$data = {
28-
readonly name: string | null;
28+
readonly name: string | null | undefined;
2929
readonly " $fragmentType": "RelayReaderNamedFragmentsTest_user";
3030
};
3131
export type RelayReaderNamedFragmentsTest_user$key = {

compiler/crates/relay-typegen/tests/generate_typescript/fixtures/aliased-inline-fragment-spread-without-type-condition-fragment-root.expected

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ fragment Foo on User {
88
import { FragmentRefs } from "relay-runtime";
99
export type Foo$data = {
1010
readonly named_fragment: {
11-
readonly name: string | null;
12-
} | null;
11+
readonly name: string | null | undefined;
12+
} | null | undefined;
1313
readonly " $fragmentType": "Foo";
1414
};
1515
export type Foo$key = {

compiler/crates/relay-typegen/tests/generate_typescript/fixtures/aliased-inline-fragment-spread-without-type-condition-linked-field.expected

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ export type RelayReaderNamedFragmentsTest2Query$data = {
1313
readonly me: {
1414
readonly id: string;
1515
readonly named_fragment: {
16-
readonly name: string | null;
17-
} | null;
18-
} | null;
16+
readonly name: string | null | undefined;
17+
} | null | undefined;
18+
} | null | undefined;
1919
};
2020
export type RelayReaderNamedFragmentsTest2Query = {
2121
response: RelayReaderNamedFragmentsTest2Query$data;

compiler/crates/relay-typegen/tests/generate_typescript/fixtures/aliased-inline-fragment-spread-without-type-condition-query-root.expected

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ export type RelayReaderNamedFragmentsTest2Query$data = {
1313
readonly named_fragment: {
1414
readonly me: {
1515
readonly id: string;
16-
readonly name: string | null;
17-
} | null;
18-
} | null;
16+
readonly name: string | null | undefined;
17+
} | null | undefined;
18+
} | null | undefined;
1919
};
2020
export type RelayReaderNamedFragmentsTest2Query = {
2121
response: RelayReaderNamedFragmentsTest2Query$data;

compiler/crates/relay-typegen/tests/generate_typescript/fixtures/aliased-inline-fragment-spread.expected

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ export type RelayReaderNamedFragmentsTest2Query$data = {
1313
readonly me: {
1414
readonly id: string;
1515
readonly named_fragment: {
16-
readonly name: string | null;
17-
} | null;
18-
} | null;
16+
readonly name: string | null | undefined;
17+
} | null | undefined;
18+
} | null | undefined;
1919
};
2020
export type RelayReaderNamedFragmentsTest2Query = {
2121
response: RelayReaderNamedFragmentsTest2Query$data;

compiler/crates/relay-typegen/tests/generate_typescript/fixtures/custom-scalar-type-import.expected

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ query Viewer($params: JSON) {
1313
==================================== OUTPUT ===================================
1414
import { JSON } from "TypeDefsFile";
1515
export type Viewer$variables = {
16-
params?: JSON | null;
16+
params?: JSON | null | undefined;
1717
};
1818
export type Viewer$data = {
1919
readonly viewer: {
2020
readonly actor: {
2121
readonly profilePicture2?: {
2222
readonly __typename: "Image";
23-
} | null;
24-
} | null;
25-
} | null;
23+
} | null | undefined;
24+
} | null | undefined;
25+
} | null | undefined;
2626
};
2727
export type Viewer = {
2828
response: Viewer$data;

0 commit comments

Comments
 (0)