proposal-json-parse-with-source

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

4posts · 30d
Nov 40 posts
  1. DEV · · @anjioo7

    Why JavaScript Silently Changes Large JSON Numbers --- title: Why JavaScript Silently Changes Large JSON Numbers published: true description: JSON.parse rounds integers above 2^53 without an error. A reproducible example, why it happens, how to detect it, and how to read big IDs exactly. tags: javascript, json, webdev, programming canonical_url: curlyjson.com --- JavaScript can silently change large integers when it parses perfectly valid JSON. That matters for database IDs, API responses, and the tools you use to look at them. Run this in Node or a browser console: const order = JSON.parse('{"orderId": 9007199254740993, "amount": 1.50}'); console.log(order.orderId); console.log(JSON.stringify(order)); Output: 9007199254740992 {"orderId":9007199254740992,"amount":1.5} The ID is now off by one. There's no exception and no warning, and the result is still valid JSON. If you pass that ID to the next request, you're asking about a different order. Where the digit goes JSON's grammar puts no limit on the size of a number. JSON.parse does: it turns every number into a JavaScript Number, which is an IEEE 754 double. A double has a 53-bit significand, so every integer up to 2^53 − 1 is stored exactly: Number.MAX_SAFE_INTEGER // 9007199254740991 Above that, only some integers fit. Between 2^53 and 2^54, a double can only hold even integers. 9007199254740993 is odd, so it's rounded to a neighbour, 9007199254740992. The same thing happens to the literal in your source code, before any JSON is involved: 9007199254740993 === 9007199254740992 // true Further up the range the gaps get wider, so the error gets bigger: JSON.parse('{"big": 12345678901234567890}').big // 12345678901234567000 Not every large integer changes: 9007199254740992 is stored exactly. The trouble is that you can't tell from the parsed value whether it was rounded. Two related changes come from the same step. 1.50 comes back as 1.5: the value is the same, but once it's a Number there's no record of the trailing zero, so JSON.stringify can't reproduce what you sent. And a number too large for a double becomes Infinity, which JSON can't represent, so it's written out as null: JSON.stringify(JSON.parse('{"d": 1e400}')) // '{"d":null}' | JSON input | After JSON.parse + JSON.stringify | What changed | |---|---|---| | 9007199254740993 | 9007199254740992 | Integer rounded | | 12345678901234567890 | 12345678901234567000 | Integer rounded | | 1.50 | 1.5 | Trailing zero dropped, value unchanged | | 1e400 | null | Too large: parsed as Infinity, which JSON can't represent | | "9007199254740993" | "9007199254740993" | Nothing: strings are kept as written | JSON itself doesn't limit number size. RFC 8259 lets implementations set their own limits, and notes that integers between −(2^53 − 1) and 2^53 − 1 are the ones every implementation using double-precision numbers will agree on. The rounding comes from JavaScript's Number, not from JSON. What's usually affected - 64-bit integer IDs (BIGINT) once their values pass 2^53, for example IDs generated from timestamps or random bits rather than a small counter. - Snowflake-style IDs. X (Twitter) and Discord both use them, and both are far above 2^53. - Numbers produced by backends with native 64-bit integers. The server serializes them correctly; the precision is lost only when JavaScript parses them. Some APIs work around it. Discord's API reference says snowflakes are always returned as strings in the HTTP API, to avoid integer overflow in some languages. Twitter's API returned every ID twice, as the number id and the string id_str, and its documentation told JavaScript developers to use id_str. Checking whether a value may have been rounded After parsing, Number.isSafeInteger tells you whether an integer is inside the exact range: Number.isSafeInteger(order.orderId) // false Number.isSafeInteger(9007199254740991) // true false means the value may have been rounded, not that it was: 9007199254740992 is itself representable. To know for sure you need the original text, and newer runtimes give it to you. The reviver function passed to JSON.parse now receives a third argument, context, whose source property is the number exactly as it was written. This comes from the TC39 proposal JSON.parse source text access, which is at Stage 4. This function finds integers outside JavaScript's safe integer range and shows their original text alongside the parsed value: function unsafeIntegers(text) { const found = []; JSON.parse(text, (key, value, context) => { if (typeof value === 'number' && Number.isInteger(value) && !Number.isSafeInteger(value)) found.push({ key, source: context.source, parsed: value }); return value; }); return found; } unsafeIntegers('{"id": 9007199254740993, "user": {"id": 42}, "ids": [12345678901234567890]}'); Output in Node 23: [ { key: 'id', source: '9007199254740993', parsed: 9007199254740992 }, { key: '0', source: '12345678901234567890', parsed: 12345678901234567000 } ] The second key is '0' because the value is the first element of the ids array. In runtimes that don't support the proposal, context is undefined, so check your target environments before relying on it. Keeping large integers exact Send them as strings. If you control the API, "orderId": "9007199254740993" is the most robust fix, because it works in every client and language. This is what Discord does. Convert them in a reviver. This approach requires a runtime that supports JSON.parse source text access. With it, you can turn a specific field into a BigInt: const text = '{"orderId": 9007199254740993, "amount": 1.50}'; const exact = JSON.parse(text, (key, value, context) => key === 'orderId' ? BigInt(context.source) : value); exact.orderId // 9007199254740993n JSON.stringify refuses to write a BigInt: TypeError: Do not know how to serialize a BigInt The same proposal adds JSON.rawJSON, which lets a replacer emit the digits unchanged: JSON.stringify(exact, (key, value) => typeof value === 'bigint' ? JSON.rawJSON(value.toString()) : value); // '{"orderId":9007199254740993,"amount":1.5}' The ID survives the round trip and amount still loses its trailing zero, because the reviver only touched orderId. MDN lists JSON.rawJSON as Baseline 2025, so it's available in current browsers but not in older ones. Use a parser built for it. Libraries such as json-bigint ("JSON.parse with bigints support") and lossless-json ("Parse JSON without risk of losing numeric information") parse numbers without going through Number. Check how each one represents numbers before choosing; their APIs differ. A quick test for JSON tools Formatters, viewers and diff tools often call JSON.parse and JSON.stringify internally. To see whether one does, paste this: { "a": 9007199254740993, "b": 12345678901234567890, "c": 1.50, "d": 1e400 } A tool built on the standard functions gives back: {"a":9007199254740992,"b":12345678901234567000,"c":1.5,"d":null} If the numeric text survives unchanged, that's a good sign the tool preserves number precision for this test. It doesn't guarantee every operation is lossless, so test the transformations you actually use. References - MDN: JSON.parse() - MDN: Number.MAX_SAFE_INTEGER - MDN: JSON.rawJSON() - TC39: JSON.parse source text access --- Written with AI assistance. Every code sample was run in Node 23, and the outputs shown are the actual results.

    1 comment
  2. Qiita · · @hashito

    Discord の ID を JSON.parse したら末尾が 063 から 060 になった。Snowflake は BigInt で読む 背景 自分のサイトに、Discord や X(旧Twitter)の ID から作成日時を取り出すツールを置いています。 作り始めたとき、テスト用の JSON を JSON.parse して ID を取り出したら、末尾の数字が変わっていました。 知識としては知っていたのですが、自分で踏むまでは他人事でした… Snowflake は、Twitter が考えた ID の作り方だ。 64 ビットの整数の上のほうにミリ秒単位の時刻、下のほうにどのサーバーで何番目に作ったかを詰める。 Discord も同じ形で、時刻の起点(エポック)だけが違う。 結論 Snowflake ID は Number で持てません。 - JSON.parse で数値のまま読むと、下位の桁が丸められて別の ID になる - 文字列のまま受け取り、ビット演算は BigInt で行う - Node.js v24 なら、JSON.parse の reviver で元の数字の文字列も取れる JavaScript の Number は小数を含む64ビットの浮動小数点数で、整数として正確に扱えるのは Number.MAX_SAFE_INTEGER(9007199254740991、約 9×10^15)までである。 Snowflake は 18〜19 桁あるので、ここを普通に超えます。 試したこと Discord の開発者ドキュメントに載っている ID の例 175928847299117063 を使いました。 docs.discord.com API から返ってくる JSON を想定して、id を数値、id_str を文字列で持たせた文字列 raw を作り、そのまま JSON.parse しています(Node.js v24.11.1)。 const raw = '{"id":175928847299117063,"id_str":"175928847299117063"}'; console.log(JSON.parse(raw)); 結果は下記でした。 { id: 175928847299117060, id_str: '175928847299117063' } 数値で読んだ id だけ、末尾が 063 から 060 になっています。 エラーも警告も出ないので、比較して初めて気づきます。 日時だけは合ってしまう ややこしいのは、壊れた数値から日時を取り出しても、ほぼ正しく見えることです。 Discord の Snowflake は、右に 22 ビットずらした値がエポック(2015-01-01 00:00:00 UTC、Unixミリ秒で 1420070400000)からのミリ秒だ。 下記は、壊れた Number と、文字列から作った BigInt の両方で日時を出したものです。 4194304 は 2 の 22 乗で、Number には >> で64ビットのシフトができないので割り算で代用しています。 const s = '175928847299117063'; // id_str(文字列) const EPOCH = 1420070400000; // Discord のエポック const viaNumber = Math.floor(Number(s) / 4194304) + EPOCH; const id = BigInt(s); const viaBigInt = Number(id >> 22n) + EPOCH; console.log(new Date(viaNumber).toISOString()); // 2016-04-30T11:18:25.796Z console.log(new Date(viaBigInt).toISOString()); // 2016-04-30T11:18:25.796Z console.log((id >> 17n) & 0x1fn, (id >> 12n) & 0x1fn, id & 0xfffn); // 1n 0n 7n 日時は両方とも 2016-04-30 11:18:25.796 UTC で、ドキュメントの例と同じになりました。 丸められるのは下位の数ビットだけで、時刻は上位に入っているので、たまたま残るという話です。 ただ、最後の行で取り出しているワーカー番号・プロセス番号・連番(ドキュメントでは 1 / 0 / 7)は下位にあるので、Number 経由だと正しく出ません。 それに、ID 自体が別の値になっているので、データベースで探しても見つからないです。 私のツールも、入力を BigInt にしてからシフトしています。 hashitosystem.com JSON から ID を正しく取り出す いちばん楽なのは、API が文字列でも返している項目(X の id_str など)を使うことです。 Discord の API は最初から ID を文字列で返すので、こちらは数値になる心配がありません。 自分で受け取る JSON が数値しか持っていないときは、JSON.parse の reviver(読み込み中に値を差し替える第2引数の関数)が使えます。 Node.js v24 では、reviver の第3引数 context.source に、数値に変換する前の元の文字列が入っていました。 const raw = '{"id":175928847299117063,"id_str":"175928847299117063"}'; const obj = JSON.parse(raw, (key, value, context) => key === 'id' && context && context.source ? BigInt(context.source) : value ); console.log(obj); // { id: 175928847299117063n, id_str: '175928847299117063' } key は今読んでいる項目名で、id のときだけ元の文字列から BigInt を作っています。 context が無い古い環境では value をそのまま返すので、少なくとも落ちはしません(その場合は丸められた値になります)。 この第3引数は TC39 の提案(JSON.parse source text access)で入ったもので、古いブラウザでは使えないかもしれません。 tc39/proposal-json-parse-with-source BigInt はそのままだと JSON.stringify で例外になるので、保存や送信のときは String(id) で文字列に戻しておくのが無難です。 Snowflake ID は数字に見えても数値として扱わない。 受け取りは文字列、計算は BigInt、と分けておけば、桁が化ける事故は起きません。 --- > 本記事はAI補助で執筆した、個人開発の紹介記事です。

  3. DEV · · @grimicorn

    JSON.parse Source Text Access: Stop Losing Precision on Big IDs Your API returns an order ID of 9007199254740993. Your frontend logs 9007199254740992. Nobody touched it. JSON.parse quietly rounded it, because every JSON number becomes a JavaScript double, and doubles only hold integers exactly up to Number.MAX_SAFE_INTEGER (9007199254740991). Anything bigger gets snapped to the nearest representable value before your code ever sees it. The usual workarounds are all a little sad. Ask the backend to send IDs as strings (id_str, if you remember the old Twitter API). Pull in a third-party parser like json-bigint. Or run a regex over the raw response to wrap long numbers in quotes before parsing, and hope nothing in a string field looks like a number. The platform now has a real fix: the JSON.parse source text access proposal, which adds a context argument to revivers and a matching JSON.rawJSON() for serialization. It reached Stage 4 at TC39 in late 2025, and it is already Baseline: Chrome 114, Firefox 135, Safari 18.4, Node 21, Deno 1.33, and Bun 1.1.43 all ship it. The reviver gets a third argument A reviver has always received (key, value). It now receives (key, value, context), and for primitive values context.source is the exact slice of JSON text that produced value. The number was still parsed into a lossy double, but you get the original digits next to it, so you can decide what it should become. const json = '{"orderId": 9007199254740993, "total": 42.5}'; const order = JSON.parse(json, (key, value, context) => { if (key === "orderId") { // value is already rounded; context.source is not return BigInt(context.source); } return value; }); order.orderId; // 9007199254740993n order.total; // 42.5 Keying off property names works when you control the schema, but a generic version is more useful in a shared fetch helper. Promote only integers that actually lost precision, and leave everything else alone: const INTEGER = /^-?\d+$/; export function preciseReviver(key, value, context) { if ( typeof value === "number" && !Number.isSafeInteger(value) && context?.source && INTEGER.test(context.source) ) { return BigInt(context.source); } return value; } const res = await fetch("/api/orders"); const data = JSON.parse(await res.text(), preciseReviver); Two details there matter. Response.json() doesn't accept a reviver, so you read the body as text and parse it yourself. And the optional chaining on context?.source is deliberate: context.source only exists for primitives. When the reviver is called for an object or array, source is absent, because there's no single meaningful slice of text to give you once the children have already been revived. Writing it back out with JSON.rawJSON Parsing is half the problem. Hand that BigInt to JSON.stringify and you get TypeError: Do not know how to serialize a BigInt. Convert it to a Number first and you're back to rounding. JSON.rawJSON() closes the loop: it wraps a piece of JSON text that stringify will emit verbatim. function preciseReplacer(key, value) { return typeof value === "bigint" ? JSON.rawJSON(value.toString()) : value; } JSON.stringify({ orderId: 9007199254740993n }, preciseReplacer); // '{"orderId":9007199254740993}' The output is a real JSON number, not a quoted string, so a Go or Java backend expecting an int64 reads it without any schema change. JSON.rawJSON is strict about what it accepts: the text must be valid JSON for a primitive, so JSON.rawJSON('[1, 2]') or a string containing a raw line break throws a SyntaxError. The returned object is frozen with a null prototype, and JSON.isRawJSON() tells you if you're holding one. Decimals, not just big integers Money is the other place this pays off. 0.1 parsed as a double is not exactly one tenth, and a payments API that returns "amount": 19.99 hands you a value that's already slightly wrong before your decimal library sees it. With context.source you can feed the original digits straight into whatever arbitrary-precision type you use: import Decimal from "decimal.js"; const invoice = JSON.parse(body, (key, value, context) => key === "amount" ? new Decimal(context.source) : value, ); On the way out, JSON.rawJSON(amount.toFixed(2)) writes 19.99 as an unquoted number again. When not to reach for it A reviver runs once per value in the document, so it is measurably slower than a plain JSON.parse on large payloads. If your IDs are already strings, or every number fits in the safe range, you don't need any of this. In TypeScript, older lib definitions may not know about the third reviver argument or JSON.rawJSON yet, so you might need a small ambient declaration until your lib setting catches up. For older browsers, core-js ships a polyfill for both halves. The bigger change is where the fix lives. Precision used to be something you negotiated with the backend team or patched with a dependency. Now it's a few lines in the one fetch helper every request already goes through. MDN's pages on JSON.parse() and JSON.rawJSON() have the full reference, and the TC39 proposal covers the design reasoning.

    1 reaction
  4. Twitter Added id_str in 2010 Because JavaScript Can't Hold a Java long - Book: Kotlin and Java to TypeScript - Also by me: The TypeScript Library — the 5-book collection - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- On October 18, 2010, Twitter told every developer on its API that tweet IDs were about to change shape. A new generator called Snowflake would produce 64-bit IDs, and TechCrunch's write-up of the notice put the date plainly: "on November 26, these IDs will go longer than 53 bits." The fix Twitter shipped was blunt. Starting October 22, every ID in a JSON response would come back twice, once as a number and once as a string. In the words of developer advocate Matt Harris, quoted in the same article: "To allow javascript and JSON parsers to read the IDs we need to include a string version of any ID." That string field is id_str, and it is still in the API. The current X developer docs tell v1.1 clients to "always use id_str", and v2 skipped the number entirely: IDs come back as strings by default. Sixteen years later, the same bug still ships. A Spring Boot or Ktor service returns a Long ID, a TypeScript client calls JSON.parse, and the last few digits change. No exception. No warning in the console. The client then sends the wrong ID back to the server, and you get a 404 on a record that exists, or a hit on a different record whose ID happens to be the rounded value. Why Twitter needed 64 bits in the first place Snowflake was built for Twitter's move from MySQL to Cassandra. The original README is direct about the motive: "there is no sequential id generation facility in Cassandra, nor should there be." The requirements were uncoordinated generation across machines, rough time ordering, and IDs that stay under 64 bits. The layout it settled on: - 41 bits of millisecond timestamp against a custom epoch ("gives us 69 years") - 10 bits of machine ID, for up to 1024 machines - 12 bits of sequence number, rolling over every 4096 IDs per machine That is 63 bits of payload, which fits a signed 64-bit integer: a Java long, a Kotlin Long, a Scala Long. The Java Language Specification gives long the range -9223372036854775808 to 9223372036854775807. Every JVM backend can hold a Snowflake ID without thinking about it. The trouble starts when the ID leaves the JVM. !A 64-bit integer bar being squeezed into a shorter 53-slot container, with the extra bits spilling off the end A JVM long has 64 bits to spend. A JavaScript number has 53 for the integer part. 53 bits of mantissa versus 64 bits of long A JavaScript number is an IEEE 754 double. MDN's page on Number.MAX_SAFE_INTEGER spells out the consequence: a double "only has 52 bits to represent the mantissa," plus one implicit leading bit, so it can only safely represent integers between -(2^53 - 1) and 2^53 - 1. That ceiling is 9007199254740991. Past it, a double still exists for every power of two, but not for every integer. Just above 2^53 the gap between neighbouring doubles is 2. Just above 2^60 it is 2^(60-52), which is 256. Modern Snowflake IDs live up there, so a parsed ID snaps to the nearest representable double, and two real IDs generated a moment apart can land on the same one. You can see it in four lines: const body = '{"id": 9007199254740993}'; const order = JSON.parse(body) as { id: number }; console.log(order.id); // 9007199254740992 console.log(Number.isSafeInteger(order.id)); // false MDN shows the same collapse from the arithmetic side: Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 evaluates to true. The X docs use 10765432100123456789, which is past even a Java long's ceiling, and show it printing back as 10765432100123458000. None of this breaks the JSON spec. RFC 8259, section 6 "allows implementations to set limits on the range and precision of numbers accepted," and names only the integers in [-(2^53)+1, (2^53)-1] as the range where implementations "will agree exactly on their numeric values." Jackson writing 9007199254740993 is valid JSON. JSON.parse rounding it is also valid. The contract between them is the bug. TypeScript does not help here. JSON.parse returns any, your as { id: number } is a promise you made to the compiler, and the value already satisfies number. The type system checks the shape, and the shape is fine. Why BigInt doesn't fix JSON.parse The natural reaction from a JVM developer is: JavaScript has arbitrary-precision integers now, so parse into BigInt. Two things get in the way. First, JSON.parse never produces a BigInt. Numbers become doubles during parsing, before your code sees anything. The MDN JSON.parse page is explicit that the reviver "is run after the value is parsed," so numbers "will have already been converted to JavaScript numbers, and may lose precision in the process." Writing BigInt(value) inside a classic two-argument reviver gives you an exact BigInt of the wrong number. Second, the trip back is closed by default. Per MDN's BigInt page, calling JSON.stringify() on any value containing a BigInt throws a TypeError unless you supply a toJSON or a replacer. So BigInt is a fine type to hold an ID once you have the exact digits. It cannot recover digits the parser already threw away. The digits have to survive the parse, which means either they arrive as a string, or the parser hands you the source text. !Two different ID cards on a conveyor belt merging into a single card as they pass through a funnel Above 2^53, distinct IDs can parse to the same JavaScript number. Fix it on the JVM side: make the ID a string in the contract The fix Twitter chose, and the one Discord documents for its own Snowflake IDs ("they are always returned as strings in the HTTP API"), is to put the ID on the wire as a JSON string. Keep Long in the database and in your domain model. Change only the serialized form. With Jackson, a per-field annotation does it: import com.fasterxml.jackson.annotation.JsonFormat; public record OrderDto( @JsonFormat(shape = JsonFormat.Shape.STRING) long id, long totalCents ) {} In Jackson 2, the number serializers return ToStringSerializer.instance when the shape is STRING, so id goes out as "9007199254740993" with the quotes. The annotation lives in jackson-annotations, which the Spring team notes kept its package unchanged when Spring Boot 4 moved to Jackson 3 under tools.jackson, so the same line works across that upgrade. If every Long in your API is an identifier, you can register the serializer by type instead. On Spring Boot 3 with Jackson 2: @Bean Jackson2ObjectMapperBuilderCustomizer longsAsStrings() { return b -> b .serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance); } Be careful with that one. It also quotes counts, cents and timestamps that never needed it. Jackson's blunter switch, JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS (off by default), "forces all regular number values to be written as JSON Strings," doubles included. Per field is usually the honest scope. On Ktor with kotlinx.serialization, the library ships a serializer for exactly this case. The LongAsStringSerializer docs describe it as "intended to be used for interoperability with external clients (mainly JavaScript ones)": import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.LongAsStringSerializer @Serializable data class OrderDto( @Serializable(with = LongAsStringSerializer::class) val id: Long, val totalCents: Long, ) Then write it down. If you publish an OpenAPI spec, the ID is type: string, not type: integer, format: int64, so generated TypeScript clients get string too. Fix it in the TypeScript client On the client, type the ID as what it is on the wire: a string. A branded type keeps it from mixing with other strings: type OrderId = string & { readonly __brand: "OrderId" }; interface Order { id: OrderId; totalCents: number; } You compare it, put it in URLs and send it back. You almost never do arithmetic on an ID, so a string loses nothing. If you need ordering or math, convert with BigInt(order.id) at that moment, from exact digits. Sometimes you don't own the server and it still sends bare numbers. For that case, TC39 added source text access to JSON.parse; the proposal is at stage 4. For primitive values, the reviver gets a third context argument whose source property is "the original JSON string representing this value" (MDN). MDN's compatibility data lists it in Chrome 114, Firefox 135, Safari 18.4, Node.js 21, Deno 1.33 and Bun 1.1.43. type Ctx = { source?: string }; function idsAsStrings(key: string, value: unknown, ctx?: Ctx) { if (key !== "id" || typeof value !== "number") return value; if (ctx?.source === undefined) { throw new Error("JSON.parse source access not supported"); } return ctx.source; // the exact digits, before rounding } const order = JSON.parse(text, idsAsStrings) as Order; The reviver throws when the runtime doesn't pass context, because falling back to String(value) would hand you the rounded digits as a string, which is the original bug wearing a disguise. And you need the raw text to call it: Response.json() takes no reviver, so read the body with await res.text() and parse it yourself. The same proposal added JSON.rawJSON for the outbound direction. A replacer like typeof v === "bigint" ? JSON.rawJSON(v.toString()) : v lets JSON.stringify emit a BigInt as a bare JSON number without precision loss, when a server insists on numeric IDs in request bodies. If you own the server, the one-line @JsonFormat change is the whole fix, and it costs less to ship before your ID generator crosses 2^53 than after the first 404 on a record that exists. --- If this was useful This bug lives exactly on the seam between the JVM and TypeScript: a Long that is perfectly safe on one side and quietly lossy on the other. Kotlin and Java to TypeScript is the book I wrote for that seam, mapping what you already know from Java and Kotlin (variance, null safety, sealed hierarchies, coroutines) onto how TypeScript does the same jobs. It's part of The TypeScript Library, five books with a clear path through them. Books 1 and 2 are the core path. If you come from the JVM, book 3 stands in for book 1. If you ship TypeScript at work, book 5 is the production layer: - TypeScript Essentials — types, narrowing, modules, async, daily-driver tooling - The TypeScript Type System — generics, mapped/conditional types, infer, template literals, branded types - Kotlin and Java to TypeScript — JVM developers moving to TypeScript - PHP to TypeScript — PHP 8+ developers moving to TypeScript - TypeScript in Production — tsconfig, build tools, monorepos, library authoring, dual ESM/CJS, JSR All five books ship in ebook, paperback, and hardcover. ![The TypeScript Library — the 5-book collection](xgabriel.com)

    1 reaction · 1 comment

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

Scan pending.

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
tc39/proposal-json-parse-with-source mentions in the last 30 days