Skip to content

Commit

Permalink
support TypeScript satisfies (#2509)
Browse files Browse the repository at this point in the history
  • Loading branch information
magic-akari committed Nov 2, 2022
1 parent 97d6dba commit 8c2fdc2
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 2 deletions.
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

0 comments on commit 8c2fdc2

Please sign in to comment.