Skip to content

Commit

Permalink
[flow] Introduce experimental.ts_syntax config to allow some TS syntax
Browse files Browse the repository at this point in the history
Summary:
Changelog: [feature]

We introduced `experimental.ts_syntax` flag that you can set to `true`. When this flag is on, Flow will no longer complain about certain TypeScript syntax, and it would automatically turn them into the closest Flow equivalent types.

Initially, we support the following: `Readonly`, `ReadonlyArray`, `ReadonlyMap`, `ReadonlySet`, `NonNullable`, and using `extends` to specify type parameter bounds. In the future, more kinds of types that we currently parse but error on might be added. (e.g. keyof)

Please note that the support here is best effort. We make no guarantee that these types have the same semantics as those in TypeScript 100% of the time, but for common cases, it might be good enough for you.

Reviewed By: gkz

Differential Revision: D53371951

fbshipit-source-id: 910454c8148b955cf6349c02edde7dec6e38b1df
  • Loading branch information
SamChou19815 authored and facebook-github-bot committed Feb 7, 2024
1 parent 0ff8dd2 commit d8bba3c
Show file tree
Hide file tree
Showing 29 changed files with 307 additions and 195 deletions.
5 changes: 5 additions & 0 deletions lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -2797,3 +2797,8 @@ type Omit<O: interface {}, Keys: $Keys<O>> = Pick<O, Exclude<$Keys<O>, Keys>>;
type Record<K: string, T> = {
[_key in K]: T;
};

// Unused if `experimental.ts_syntax` is off
type ReadonlyMap<K, +V> = $ReadOnlyMap<K, V>;
// Unused if `experimental.ts_syntax` is off
type ReadonlySet<+V> = $ReadOnlySet<V>;
1 change: 1 addition & 0 deletions src/codemods/annotate_exports.ml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ module SignatureVerification = struct
exact_by_default = Options.exact_by_default options;
enable_enums = Options.enums options;
enable_component_syntax = Options.typecheck_component_syntax_in_file options file;
enable_ts_syntax = Options.ts_syntax options;
enable_relay_integration = Options.enable_relay_integration options;
casting_syntax = Options.casting_syntax options;
relay_integration_module_prefix = Options.relay_integration_module_prefix options;
Expand Down
1 change: 1 addition & 0 deletions src/commands/commandUtils.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,7 @@ let make_options
Base.List.map
~f:(Files.expand_project_root_token ~root)
(FlowConfig.strict_es6_import_export_excludes flowconfig);
opt_ts_syntax = FlowConfig.ts_syntax flowconfig;
opt_typeof_with_type_arguments = FlowConfig.typeof_with_type_arguments flowconfig;
opt_automatic_require_default =
Base.Option.value (FlowConfig.automatic_require_default flowconfig) ~default:false;
Expand Down
5 changes: 5 additions & 0 deletions src/commands/config/flowConfig.ml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ module Opts = struct
strict_es6_import_export_excludes: string list;
suppress_types: SSet.t;
traces: int;
ts_syntax: bool;
typeof_with_type_arguments: bool;
use_mixed_in_catch_variables: bool option;
wait_for_recheck: bool;
Expand Down Expand Up @@ -254,6 +255,7 @@ module Opts = struct
strict_es6_import_export_excludes = [];
suppress_types = SSet.empty |> SSet.add "$FlowFixMe";
traces = 0;
ts_syntax = false;
typeof_with_type_arguments = false;
use_mixed_in_catch_variables = None;
wait_for_recheck = false;
Expand Down Expand Up @@ -872,6 +874,7 @@ module Opts = struct
);
("experimental.multi_platform.extensions", multi_platform_extensions_parser);
("experimental.namespaces", boolean (fun opts v -> Ok { opts with namespaces = v }));
("experimental.ts_syntax", boolean (fun opts v -> Ok { opts with ts_syntax = v }));
("experimenta.precise_dependents", precise_dependents_parser);
("facebook.fbs", string (fun opts v -> Ok { opts with facebook_fbs = Some v }));
("facebook.fbt", string (fun opts v -> Ok { opts with facebook_fbt = Some v }));
Expand Down Expand Up @@ -1654,6 +1657,8 @@ let suppress_types c = c.options.Opts.suppress_types

let traces c = c.options.Opts.traces

let ts_syntax c = c.options.Opts.ts_syntax

let typeof_with_type_arguments c = c.options.Opts.typeof_with_type_arguments

let use_mixed_in_catch_variables c = c.options.Opts.use_mixed_in_catch_variables
Expand Down
2 changes: 2 additions & 0 deletions src/commands/config/flowConfig.mli
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ val suppress_types : config -> SSet.t

val traces : config -> int

val ts_syntax : config -> bool

val typeof_with_type_arguments : config -> bool

val use_mixed_in_catch_variables : config -> bool option
Expand Down
3 changes: 3 additions & 0 deletions src/common/options.ml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ type t = {
opt_suppress_types: SSet.t;
opt_temp_dir: string;
opt_traces: int;
opt_ts_syntax: bool;
opt_typeof_with_type_arguments: bool;
opt_use_mixed_in_catch_variables: bool;
opt_verbose: Verbose.t option;
Expand Down Expand Up @@ -338,6 +339,8 @@ let suppress_types opts = opts.opt_suppress_types

let temp_dir opts = opts.opt_temp_dir

let ts_syntax opts = opts.opt_ts_syntax

let typeof_with_type_arguments opts = opts.opt_typeof_with_type_arguments

let use_mixed_in_catch_variables opts = opts.opt_use_mixed_in_catch_variables
Expand Down
2 changes: 2 additions & 0 deletions src/flow_dot_js.ml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ let load_lib_files files =
exact_by_default = true;
enable_enums = true;
enable_component_syntax = true;
enable_ts_syntax = false;
hooklike_functions = true;
casting_syntax = Options.CastingSyntax.Both;
for_builtins = true;
Expand Down Expand Up @@ -147,6 +148,7 @@ let stub_metadata ~root ~checked =
strict_es6_import_export_excludes = [];
strip_root = true;
suppress_types = SSet.of_list ["$FlowFixMe"; "$FlowIssue"; "$FlowIgnore"; "$FlowExpectedError"];
ts_syntax = false;
typeof_with_type_arguments = true;
use_mixed_in_catch_variables = false;
}
Expand Down
1 change: 1 addition & 0 deletions src/parser_utils/exports/__tests__/exports_tests.ml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ let sig_opts =
exact_by_default = true;
enable_enums = true;
enable_component_syntax = true;
enable_ts_syntax = true;
hooklike_functions = true;
enable_relay_integration = false;
casting_syntax = Options.CastingSyntax.Colon;
Expand Down
2 changes: 2 additions & 0 deletions src/parser_utils/type_sig/__tests__/type_sig_tests.ml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ let sig_options
?(exact_by_default = false)
?(enable_enums = true)
?(enable_component_syntax = true)
?(enable_ts_syntax = true)
?(hooklike_functions = true)
?(enable_relay_integration = false)
?(casting_syntax = Options.CastingSyntax.Colon)
Expand All @@ -229,6 +230,7 @@ let sig_options
exact_by_default;
enable_enums;
enable_component_syntax;
enable_ts_syntax;
hooklike_functions;
enable_relay_integration;
casting_syntax;
Expand Down
3 changes: 3 additions & 0 deletions src/parser_utils/type_sig/type_sig_options.ml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type t = {
exact_by_default: bool;
enable_enums: bool;
enable_component_syntax: bool;
enable_ts_syntax: bool;
hooklike_functions: bool;
casting_syntax: Options.CastingSyntax.t;
enable_relay_integration: bool;
Expand Down Expand Up @@ -56,6 +57,7 @@ let of_options options docblock locs_to_dirtify file =
exact_by_default = Options.exact_by_default options;
enable_enums = Options.enums options;
enable_component_syntax = Options.typecheck_component_syntax_in_file options file;
enable_ts_syntax = Options.ts_syntax options;
for_builtins = false;
casting_syntax = Options.casting_syntax options;
}
Expand All @@ -76,6 +78,7 @@ let builtin_options options =
exact_by_default = Options.exact_by_default options;
enable_enums = Options.enums options;
enable_component_syntax = true;
enable_ts_syntax = false;
hooklike_functions = Options.hooklike_functions options;
casting_syntax = Options.casting_syntax options;
for_builtins = true;
Expand Down
29 changes: 29 additions & 0 deletions src/parser_utils/type_sig/type_sig_parse.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2405,6 +2405,35 @@ and maybe_special_unqualified_generic opts scope tbls xs loc targs ref_loc =
| None -> Annot (FlowDebugSleep loc)
| _ -> Err (loc, CheckError)
end
| "Readonly" ->
if opts.enable_ts_syntax then
match targs with
| Some (_, { arguments = [t]; _ }) ->
let t = annot opts scope tbls xs t in
Annot (ReadOnly (loc, t))
| _ -> Err (loc, CheckError)
else
Annot (Any loc)
| "ReadonlyArray" ->
if opts.enable_ts_syntax then
match targs with
| Some (_, { arguments = [t]; _ }) ->
let t = annot opts scope tbls xs t in
Annot (ReadOnlyArray (loc, t))
| _ -> Err (loc, CheckError)
else
Annot (Any loc)
| "NonNullable" ->
if opts.enable_ts_syntax then
match targs with
| Some (_, { arguments = [t]; _ }) ->
let t = annot opts scope tbls xs t in
Annot (NonMaybeType (loc, t))
| _ -> Err (loc, CheckError)
else
Annot (Any loc)
| "ReadonlyMap" when not opts.enable_ts_syntax -> Annot (Any loc)
| "ReadonlySet" when not opts.enable_ts_syntax -> Annot (Any loc)
| name ->
let name = Unqualified (Ref { ref_loc; name; scope; resolved = None }) in
nominal_type opts scope tbls xs loc name targs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ let stub_metadata ~root ~checked =
strict_es6_import_export_excludes = [];
strip_root = true;
suppress_types = SSet.empty;
ts_syntax = true;
typeof_with_type_arguments = true;
use_mixed_in_catch_variables = false;
}
Expand Down
2 changes: 2 additions & 0 deletions src/typing/__tests__/type_hint_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ let metadata =
strict_es6_import_export_excludes = [];
strip_root = true;
suppress_types = SSet.empty;
ts_syntax = true;
typeof_with_type_arguments = true;
use_mixed_in_catch_variables = false;
}
Expand Down Expand Up @@ -194,6 +195,7 @@ end = struct
exact_by_default = true;
enable_enums = true;
enable_component_syntax = true;
enable_ts_syntax = true;
hooklike_functions = true;
casting_syntax = Options.CastingSyntax.Both;
for_builtins = true;
Expand Down
1 change: 1 addition & 0 deletions src/typing/__tests__/typed_ast_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ let metadata =
strict_es6_import_export_excludes = [];
strip_root = true;
suppress_types = SSet.empty;
ts_syntax = true;
typeof_with_type_arguments = true;
use_mixed_in_catch_variables = false;
}
Expand Down
4 changes: 4 additions & 0 deletions src/typing/context.ml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type metadata = {
strict_es6_import_export_excludes: string list;
strip_root: bool;
suppress_types: SSet.t;
ts_syntax: bool;
typeof_with_type_arguments: bool;
use_mixed_in_catch_variables: bool;
}
Expand Down Expand Up @@ -280,6 +281,7 @@ let metadata_of_options options =
strict_es6_import_export_excludes = Options.strict_es6_import_export_excludes options;
strip_root = Options.should_strip_root options;
suppress_types = Options.suppress_types options;
ts_syntax = Options.ts_syntax options;
typeof_with_type_arguments = Options.typeof_with_type_arguments options;
use_mixed_in_catch_variables = Options.use_mixed_in_catch_variables options;
}
Expand Down Expand Up @@ -563,6 +565,8 @@ let should_strip_root cx = cx.metadata.strip_root

let suppress_types cx = cx.metadata.suppress_types

let ts_syntax cx = cx.metadata.ts_syntax

let literal_subtypes cx = cx.ccx.literal_subtypes

let post_inference_polarity_checks cx = cx.ccx.post_inference_polarity_checks
Expand Down
3 changes: 3 additions & 0 deletions src/typing/context.mli
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type metadata = {
strict_es6_import_export_excludes: string list;
strip_root: bool;
suppress_types: SSet.t;
ts_syntax: bool;
typeof_with_type_arguments: bool;
use_mixed_in_catch_variables: bool;
}
Expand Down Expand Up @@ -249,6 +250,8 @@ val should_strip_root : t -> bool

val suppress_types : t -> SSet.t

val ts_syntax : t -> bool

val type_graph : t -> Graph_explorer.graph

val matching_props : t -> (string * ALoc.t * ALoc.t) list
Expand Down
71 changes: 51 additions & 20 deletions src/typing/type_annotation.ml
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ module Make (ConsGen : Type_annotation_sig.ConsGen) (Statement : Statement_sig.S
in

let use_op reason = Op (TypeApplication { type_ = reason }) in
let local_generic_type () =
let local_generic_type ?(name = name) () =
let reason = mk_reason (RType (OrdinaryName name)) loc in
let c = type_identifier cx name name_loc in
let (t, targs) =
Expand Down Expand Up @@ -1285,37 +1285,68 @@ module Make (ConsGen : Type_annotation_sig.ConsGen) (Statement : Statement_sig.S
| "$Flow$DebugSleep" -> mk_custom_fun cx loc t_ast targs ident DebugSleep
(* TS Types *)
| "Readonly" ->
error_type
cx
loc
Error_message.(EIncorrectTypeWithReplacement { loc; kind = IncorrectType.TSReadonly })
t_ast
if Context.ts_syntax cx then
check_type_arg_arity cx loc t_ast targs 1 (fun () ->
let (ts, targs) = convert_type_params () in
let t = List.hd ts in
let reason = mk_reason RReadOnlyType loc in
reconstruct_ast
(mk_type_destructor cx (use_op reason) reason t ReadOnlyType (mk_eval_id cx loc))
targs
)
else
error_type
cx
loc
Error_message.(EIncorrectTypeWithReplacement { loc; kind = IncorrectType.TSReadonly })
t_ast
| "ReadonlyArray" ->
error_type
cx
loc
Error_message.(
EIncorrectTypeWithReplacement { loc; kind = IncorrectType.TSReadonlyArray }
if Context.ts_syntax cx then
check_type_arg_arity cx loc t_ast targs 1 (fun () ->
let (elemts, targs) = convert_type_params () in
let elemt = List.hd elemts in
reconstruct_ast
(DefT (mk_annot_reason RROArrayType loc, ArrT (ROArrayAT (elemt, None))))
targs
)
t_ast
| "ReadonlyMap" ->
else
error_type
cx
loc
Error_message.(
EIncorrectTypeWithReplacement { loc; kind = IncorrectType.TSReadonlyArray }
)
t_ast
| "ReadonlyMap" when not (Context.ts_syntax cx) ->
error_type
cx
loc
Error_message.(EIncorrectTypeWithReplacement { loc; kind = IncorrectType.TSReadonlyMap })
t_ast
| "ReadonlySet" ->
| "ReadonlySet" when not (Context.ts_syntax cx) ->
error_type
cx
loc
Error_message.(EIncorrectTypeWithReplacement { loc; kind = IncorrectType.TSReadonlySet })
t_ast
| "NonNullable" ->
error_type
cx
loc
Error_message.(EIncorrectTypeWithReplacement { loc; kind = IncorrectType.TSNonNullable })
t_ast
if Context.ts_syntax cx then
check_type_arg_arity cx loc t_ast targs 1 (fun () ->
let (ts, targs) = convert_type_params () in
let t = List.hd ts in
let reason = mk_reason (RType (OrdinaryName "NonNullable")) loc in
reconstruct_ast
(mk_type_destructor cx (use_op reason) reason t NonMaybeType (mk_eval_id cx loc))
targs
)
else
error_type
cx
loc
Error_message.(
EIncorrectTypeWithReplacement { loc; kind = IncorrectType.TSNonNullable }
)
t_ast
(* other applications with id as head expr *)
| _ -> local_generic_type ()
end
Expand Down Expand Up @@ -2552,7 +2583,7 @@ module Make (ConsGen : Type_annotation_sig.ConsGen) (Statement : Statement_sig.S
let reason = mk_annot_reason (RType (OrdinaryName name)) name_loc in
let polarity = polarity cx variance in
(match bound_kind with
| Ast.Type.TypeParam.Extends when not from_infer_type ->
| Ast.Type.TypeParam.Extends when not (from_infer_type || Context.ts_syntax cx) ->
Flow_js_utils.add_output
cx
(Error_message.ETSSyntax { kind = Error_message.TSTypeParamExtends; loc });
Expand Down
3 changes: 3 additions & 0 deletions tests/ts_syntax/.flowconfig
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[libs]
unused_ts_shim.js

[options]
all=true
1 change: 1 addition & 0 deletions tests/ts_syntax/readonlymap.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
type T = ReadonlyMap<string, number>; // ERROR
const x: T = 3; // ok, since T interpreted to be any when experimental.ts_syntax is off
2 changes: 2 additions & 0 deletions tests/ts_syntax/unused_ts_shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Intentionally bad shim for ts names to demonstrate that it will not be read if `experimental.ts_syntax` is off
type ReadonlyMap<K, +V> = string;
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
[libs]
shim.js

[options]
experimental.ts_syntax=true
all=true
no_flowlib=false
6 changes: 6 additions & 0 deletions tests/ts_syntax_allowed/exported.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const a: ReadonlyArray<number> = [3]; // ok
export const b: ReadonlySet<number> = new Set([3]); // ok
export const c: ReadonlyMap<number, string> = new Map([[3, '']]); // ok
export const d: NonNullable<string | null> = ''; // ok
export const e: Readonly<{foo: string}> = {foo: ''}; // ok
export function f<T extends string>(t: T): void {} // ok
3 changes: 3 additions & 0 deletions tests/ts_syntax_allowed/tparam_bound.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function f<T extends string>(t: T): void {} // ok
f(1); // error: number ~> string
require('./exported').f(1); // error: number ~> string
Loading

0 comments on commit d8bba3c

Please sign in to comment.