From 3d99ad456a725238af8622af7a6c210a636de80a Mon Sep 17 00:00:00 2001 From: magic-akari Date: Wed, 31 Aug 2022 20:27:11 +0800 Subject: [PATCH 1/2] support TypeScript `satisfies` --- internal/js_parser/js_parser.go | 4 +-- internal/js_parser/ts_parser_test.go | 50 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 1c2df9aa393..a8ea8fceec1 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -4376,8 +4376,8 @@ func (p *parser) parseSuffix(left js_ast.Expr, level js_ast.L, errors *deferredE left = js_ast.Expr{Loc: left.Loc, Data: &js_ast.EBinary{Op: js_ast.BinOpInstanceof, Left: left, Right: p.parseExpr(js_ast.LCompare)}} default: - // Handle the TypeScript "as" operator - if p.options.ts.Parse && level < js_ast.LCompare && !p.lexer.HasNewlineBefore && p.lexer.IsContextualKeyword("as") { + // Handle the TypeScript "as"/"satisfies" operator + if p.options.ts.Parse && level < js_ast.LCompare && !p.lexer.HasNewlineBefore && (p.lexer.IsContextualKeyword("as") || p.lexer.IsContextualKeyword("satisfies")) { p.lexer.Next() p.skipTypeScriptType(js_ast.LLowest) diff --git a/internal/js_parser/ts_parser_test.go b/internal/js_parser/ts_parser_test.go index 4566a637cbe..acbc911a261 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -403,6 +403,56 @@ func TestTSAsCast(t *testing.T) { expectParseErrorTS(t, "(x = y as any(z));", ": ERROR: Expected \")\" but found \"(\"\n") } +func TestTsSatisfies(t *testing.T) { + expectPrintedTS(t, "const t1 = { a: 1 } satisfies I1;", "const t1 = { a: 1 };\n") + expectPrintedTS(t, "const t2 = { a: 1, b: 1 } satisfies I1;", "const t2 = { a: 1, b: 1 };\n") + expectPrintedTS(t, "const t3 = { } satisfies I1;", "const t3 = {};\n") + expectPrintedTS(t, "const t4: T1 = { a: 'a' } satisfies T1;", "const t4 = { a: \"a\" };\n") + expectPrintedTS(t, "const t5 = (m => m.substring(0)) satisfies T2;", "const t5 = (m) => m.substring(0);\n") + expectPrintedTS(t, "const t6 = [1, 2] satisfies [number, number];", "const t6 = [1, 2];\n") + expectPrintedTS(t, "let t7 = { a: 'test' } satisfies A;", "let t7 = { a: \"test\" };\n") + expectPrintedTS(t, "let t8 = { a: 'test', b: 'test' } satisfies A;", "let t8 = { a: \"test\", b: \"test\" };\n") + expectPrintedTS(t, "export default {} satisfies Foo;", "export default {};\n") + expectPrintedTS(t, "export default { a: 1 } satisfies Foo;", "export default { a: 1 };\n") + expectPrintedTS(t, + "const p = { isEven: n => n % 2 === 0, isOdd: n => n % 2 === 1 } satisfies Predicates;", + "const p = { isEven: (n) => n % 2 === 0, isOdd: (n) => n % 2 === 1 };\n") + expectPrintedTS(t, + "let obj: { f(s: string): void } & Record = { f(s) { }, g(s) { } } satisfies { g(s: string): void } & Record;", + "let obj = { f(s) {\n}, g(s) {\n} };\n") + expectPrintedTS(t, + "const car = { start() { }, move(d) { }, stop() { } } satisfies Movable & Record;", + "const car = { start() {\n}, move(d) {\n}, stop() {\n} };\n", + ) + expectPrintedTS(t, "var v = undefined satisfies 1;", "var v = void 0;\n") + expectPrintedTS(t, "const a = { x: 10 } satisfies Partial;", "const a = { x: 10 };\n") + expectPrintedTS(t, + "const p = { a: 0, b: \"hello\", x: 8 } satisfies Partial>;", + "const p = { a: 0, b: \"hello\", x: 8 };\n", + ) + expectPrintedTS(t, + "const p = { a: 0, b: \"hello\", x: 8 } satisfies Record;", + "const p = { a: 0, b: \"hello\", x: 8 };\n", + ) + expectPrintedTS(t, + "const x2 = { m: true, s: \"false\" } satisfies Facts;", + "const x2 = { m: true, s: \"false\" };\n", + ) + expectPrintedTS(t, + "export const Palette = { white: { r: 255, g: 255, b: 255 }, black: { r: 0, g: 0, d: 0 }, blue: { r: 0, g: 0, b: 255 }, } satisfies Record;", + "export const Palette = { white: { r: 255, g: 255, b: 255 }, black: { r: 0, g: 0, d: 0 }, blue: { r: 0, g: 0, b: 255 } };\n", + ) + expectPrintedTS(t, + "const a: \"baz\" = \"foo\" satisfies \"foo\" | \"bar\";", + "const a = \"foo\";\n", + ) + expectPrintedTS(t, + "const b: { xyz: \"baz\" } = { xyz: \"foo\" } satisfies { xyz: \"foo\" | \"bar\" };", + "const b = { xyz: \"foo\" };\n", + ) + +} + func TestTSClass(t *testing.T) { expectPrintedTS(t, "export default class Foo {}", "export default class Foo {\n}\n") expectPrintedTS(t, "export default class Foo extends Bar {}", "export default class Foo extends Bar {\n}\n") From 04235ea6061c1e2cc44799ee70d984562abb6fa8 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Wed, 2 Nov 2022 16:45:50 -0400 Subject: [PATCH 2/2] release notes --- CHANGELOG.md | 20 ++++++++++++++++++++ internal/js_parser/ts_parser_test.go | 3 +-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f108703fd..d5b80169c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ ## Unreleased +* Add support for the TypeScript 4.9 `satisfies` operator ([#2509](https://github.com/evanw/esbuild/pull/2509)) + + TypeScript 4.9 introduces a new operator called `satisfies` that lets you check that a given value satisfies a less specific type without casting it to that less specific type and without generating any additional code at run-time. It looks like this: + + ```ts + const value = { foo: 1, bar: false } satisfies Record + console.log(value.foo.toFixed(1)) // TypeScript knows that "foo" is a number here + ``` + + Before this existed, you could use a cast with `as` to check that a value satisfies a less specific type, but that removes any additional knowledge that TypeScript has about that specific value: + + ```ts + const value = { foo: 1, bar: false } as Record + console.log(value.foo.toFixed(1)) // TypeScript no longer knows that "foo" is a number + ``` + + You can read more about this feature in [TypeScript's blog post for 4.9](https://devblogs.microsoft.com/typescript/announcing-typescript-4-9-rc/#the-satisfies-operator) as well as [the associated TypeScript issue for this feature](https://github.com/microsoft/TypeScript/issues/47920). + + This feature was implemented in esbuild by [@magic-akari](https://github.com/magic-akari). + * Fix watch mode constantly rebuilding if the parent directory is inaccessible ([#2640](https://github.com/evanw/esbuild/issues/2640)) Android is unusual in that it has an inaccessible directory in the path to the root, which esbuild was not originally built to handle. To handle cases like this, the path resolution layer in esbuild has a hack where it treats inaccessible directories as empty. However, esbuild's watch implementation currently triggers a rebuild if a directory previously encountered an error but the directory now exists. The assumption is that the previous error was caused by the directory not existing. Although that's usually the case, it's not the case for this particular parent directory on Android. Instead the error is that the directory previously existed but was inaccessible. diff --git a/internal/js_parser/ts_parser_test.go b/internal/js_parser/ts_parser_test.go index acbc911a261..1dc89d250b7 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -403,7 +403,7 @@ func TestTSAsCast(t *testing.T) { expectParseErrorTS(t, "(x = y as any(z));", ": ERROR: Expected \")\" but found \"(\"\n") } -func TestTsSatisfies(t *testing.T) { +func TestTSSatisfies(t *testing.T) { expectPrintedTS(t, "const t1 = { a: 1 } satisfies I1;", "const t1 = { a: 1 };\n") expectPrintedTS(t, "const t2 = { a: 1, b: 1 } satisfies I1;", "const t2 = { a: 1, b: 1 };\n") expectPrintedTS(t, "const t3 = { } satisfies I1;", "const t3 = {};\n") @@ -450,7 +450,6 @@ func TestTsSatisfies(t *testing.T) { "const b: { xyz: \"baz\" } = { xyz: \"foo\" } satisfies { xyz: \"foo\" | \"bar\" };", "const b = { xyz: \"foo\" };\n", ) - } func TestTSClass(t *testing.T) {