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/js_parser.go b/internal/js_parser/js_parser.go index 3be17b863b4..079290da31d 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -4405,8 +4405,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..1dc89d250b7 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -403,6 +403,55 @@ 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")