Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support TypeScript satisfies #2509

Merged
merged 3 commits into from Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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<string, number | boolean>
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<string, number | boolean>
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.
Expand Down
4 changes: 2 additions & 2 deletions internal/js_parser/js_parser.go
Expand Up @@ -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)

Expand Down
49 changes: 49 additions & 0 deletions internal/js_parser/ts_parser_test.go
Expand Up @@ -403,6 +403,55 @@ func TestTSAsCast(t *testing.T) {
expectParseErrorTS(t, "(x = y as any(z));", "<stdin>: 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<string, unknown> = { f(s) { }, g(s) { } } satisfies { g(s: string): void } & Record<string, unknown>;",
"let obj = { f(s) {\n}, g(s) {\n} };\n")
expectPrintedTS(t,
"const car = { start() { }, move(d) { }, stop() { } } satisfies Movable & Record<string, unknown>;",
"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<Point2d>;", "const a = { x: 10 };\n")
expectPrintedTS(t,
"const p = { a: 0, b: \"hello\", x: 8 } satisfies Partial<Record<Keys, unknown>>;",
"const p = { a: 0, b: \"hello\", x: 8 };\n",
)
expectPrintedTS(t,
"const p = { a: 0, b: \"hello\", x: 8 } satisfies Record<Keys, unknown>;",
"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<string, Color>;",
"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<T> {}", "export default class Foo extends Bar {\n}\n")
Expand Down