TypeScript vs JavaScript — Specification¶
Official Documentation Reference
Source: TypeScript Handbook — The Basics and TypeScript for JavaScript Programmers
Table of Contents¶
- Docs Reference
- The Superset Relationship
- Static vs Dynamic Typing (Per the Handbook)
- Type Erasure — The Defining Property
- Structural Typing
- What Compiles Away vs What Emits
- The Compilation Pipeline
- Type Inference Rules
any,unknown, and the Escape Hatches- Soundness — Where the Type System Is Deliberately Unsafe
- Official Examples
- Edge Cases from the Docs
- Version and History Notes
- Compliance Checklist
- Related Documentation
1. Docs Reference¶
| Property | Value |
|---|---|
| Official Site | typescriptlang.org |
| 5-Minute Intro | TypeScript for JavaScript Programmers |
| Handbook — Basics | The Basics |
| Everyday Types | Everyday Types |
| Type Compatibility | Type Compatibility |
| Playground | TS Playground — shows emitted JS |
| Design Goals | TypeScript Design Goals (Wiki) |
The official documentation frames TypeScript as "JavaScript with syntax for types." This specification page mirrors the Handbook's framing of the TS↔JS relationship, with direct links and precise behavioral notes for each concept.
From the official site: "TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale."
2. The Superset Relationship¶
Reference: TypeScript for JavaScript Programmers
The single foundational claim of the language: TypeScript is a typed superset of JavaScript. Formally, the set of valid TypeScript programs is a superset of the set of valid JavaScript programs.
| Claim | Meaning |
|---|---|
| Every valid JS is valid TS | A .js file's contents, renamed to .ts, parses and (usually) compiles |
| TS adds syntax | Type annotations, interfaces, enums, generics, as, satisfies — none of which exist in JS |
| TS emits JS | The output of the compiler is plain JavaScript at a configurable language level |
From the Design Goals: "Statically identify constructs that are likely to be errors" and "Impose no runtime overhead on emitted programs."
The one caveat the docs note¶
The "all JS is valid TS" claim is true at the syntax level. With type checking enabled, valid JS may still produce type errors (e.g. an implicit any under noImplicitAny). The program is still valid TypeScript syntax; the compiler simply reports diagnostics. By default tsc still emits the JS even with errors (see noEmitOnError).
// 100% valid JavaScript. As TS, it parses fine.
// Under "noImplicitAny", `name` triggers a diagnostic — but it is still valid TS syntax.
function greet(name) {
return "Hello, " + name;
}
Design goal: a superset, not a replacement¶
The TypeScript team's stated non-goals explicitly include: "Do not add expression-level syntax" in ways that would change JavaScript semantics, and "Use a consistent, fully erasable, structural type system." The word erasable is the key — it guarantees the superset relationship is one-directional and lossless on the way down to JS.
3. Static vs Dynamic Typing (Per the Handbook)¶
Reference: The Basics
The Handbook's "The Basics" page opens by contrasting the two models directly.
| Aspect | JavaScript (dynamic) | TypeScript (static) |
|---|---|---|
| When types are known | At runtime, per value | At compile time, per expression |
| When mistakes surface | When the bad line executes | When tsc checks the file |
| Tooling depth | Limited (guesswork) | Rich (the checker knows every type) |
| Cost model | Pay at runtime via crashes | Pay once at build time |
From the Handbook: "Ideally, we could have a tool that helps us find these bugs before our code runs. That's what a static type-checker like TypeScript does."
The canonical Handbook example¶
// JavaScript behavior the Handbook highlights:
const message = "Hello World!";
// message(); // In plain JS: runtime TypeError: message is not a function
// // In TS: compile-time error before you ever run it:
// // "This expression is not callable. Type 'String' has no call signatures."
The Handbook stresses that TypeScript flags this statically — the red squiggle appears in the editor the moment you type it, not when a user triggers the code path in production.
Catching typos — another Handbook example¶
const user = {
name: "Daniel",
age: 26,
};
// JavaScript: undefined, silently — a typo bug that ships
// user.location;
// TypeScript: compile error
// Property 'location' does not exist on type '{ name: string; age: number; }'.
4. Type Erasure — The Defining Property¶
Reference: TypeScript for JavaScript Programmers — "Types by Inference" / erasure and Design Goal "fully erasable type system."
This is the most consequential rule in the entire language for understanding the TS↔JS relationship.
Rule: All type-only constructs are erased during compilation. The emitted JavaScript contains no type annotations, interfaces, type aliases, generic parameters, or as/satisfies expressions.
Constructs that are fully erased (produce ZERO output)¶
| Construct | Emits any JS? |
|---|---|
interface | No — vanishes entirely |
type alias | No — vanishes entirely |
Type annotations (: string) | No — the : string is stripped |
Generic parameters (<T>) | No — erased to nothing |
as assertions / satisfies | No — the value remains, the assertion is stripped |
declare statements | No — ambient, type-only |
Type-only imports (import type) | No — elided |
Constructs that DO emit JavaScript (not pure types)¶
| Construct | Emits JS? | Note |
|---|---|---|
enum (non-const) | Yes | Generates a real runtime object |
class | Yes | Real runtime constructor (but field types are erased) |
namespace with values | Yes | Generates an IIFE object |
const enum | Inlined | Replaced by literal values at use sites |
Parameter properties (constructor(private x)) | Yes | Emits the assignment |
Side-by-side proof¶
// INPUT (.ts)
interface User {
id: number;
name: string;
}
type ID = string | number;
function getName(user: User): string {
return user.name;
}
const u = { id: 1, name: "Ada" } as User;
// OUTPUT (.js) — note: interface AND type alias produced nothing; `as User` stripped
"use strict";
function getName(user) {
return user.name;
}
const u = { id: 1, name: "Ada" };
Consequences of erasure (each cited as a "gotcha" in the docs)¶
- Zero runtime overhead. The emitted JS is what a JS author would hand-write. This is the Design Goal: "Impose no runtime overhead on emitted programs."
- No type reflection. You cannot ask "what is
T?" at runtime;Tdoes not exist. instanceofon an interface is impossible. Interfaces have no runtime value. The Handbook directs you to discriminated unions or classes instead.- Types are not validation. A value typed as
UserfromJSON.parseis asserted, not checked. Runtime data still needs guards (the Handbook's "Narrowing" chapter, plus libraries like Zod).
interface Shape { kind: string; }
// if (x instanceof Shape) {} // Error: 'Shape' only refers to a type, but is being used as a value here.
5. Structural Typing¶
Reference: Type Compatibility
TypeScript uses a structural type system (a.k.a. "duck typing"), in deliberate contrast to the nominal systems of Java/C#. The Design Goals state: "Use a consistent, fully erasable, structural type system."
From the docs: "Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members."
The basic rule¶
A type S is assignable to type T if S has at least the members T requires — regardless of names, declarations, or implements clauses.
interface Named {
name: string;
}
class Person {
// no "implements Named" anywhere
constructor(public name: string) {}
}
let n: Named;
n = new Person("Alice"); // OK — Person structurally has `name: string`
The "extra properties are fine for variables" rule¶
interface Point { x: number; y: number; }
const p3d = { x: 1, y: 2, z: 3 };
const p2d: Point = p3d; // OK — p3d has at least x and y
Excess property checks — the exception for object literals¶
The docs note a special case: when you assign an object literal directly, excess properties are flagged (to catch typos). Via an intermediate variable, the check is relaxed.
interface Point { x: number; y: number; }
// Direct literal — excess property check fires:
// const p: Point = { x: 1, y: 2, z: 3 };
// Error: Object literal may only specify known properties, and 'z' does not exist in type 'Point'.
// Via a variable — structural rule allows it:
const tmp = { x: 1, y: 2, z: 3 };
const p: Point = tmp; // OK
Why structural typing matters for the TS↔JS relationship¶
JavaScript objects are bags of properties created on the fly — there is no class hierarchy you must opt into. A structural system matches how JS code is actually written: you pass "anything with the right shape." A nominal system would reject the idiomatic JS pattern of plain object literals.
6. What Compiles Away vs What Emits¶
Reference: TSConfig —
targetand the Playground "JS" tab
The compiler does two distinct jobs that the docs keep separate:
- Type checking — analyze types, report diagnostics. Affects nothing in the output.
- Emit (transpilation) — strip types AND downlevel modern syntax to the configured
target.
Type stripping (always happens)¶
Independent of any option, type annotations are removed.
Syntax downleveling (depends on target)¶
// INPUT (.ts), target: ES5
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2);
class Box { value = 0; }
// OUTPUT with target ES5 — arrow fn and class downleveled to ES5
"use strict";
var nums = [1, 2, 3];
var doubled = nums.map(function (n) { return n * 2; });
var Box = (function () {
function Box() { this.value = 0; }
return Box;
}());
// OUTPUT with target ES2017 — modern syntax preserved, only types stripped
"use strict";
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2);
class Box { constructor() { this.value = 0; } }
Key distinction: type stripping is about the type system; downleveling is about JavaScript language version compatibility. Both happen during emit, but they answer different questions.
7. The Compilation Pipeline¶
| Phase | Job | Relationship to JS |
|---|---|---|
| Parse | Build AST from source | Superset grammar: JS grammar + type syntax |
| Bind | Resolve names to symbols | Same scoping rules as JS |
| Check | Verify type assignability | The only phase JS lacks |
| Emit | Produce .js (+ .d.ts, maps) | Output is plain JS at target level |
Crucially, the Check phase and the Emit phase are independent. By default, type errors do NOT stop emit — tsc reports them and still writes .js. Set noEmitOnError: true to couple them.
8. Type Inference Rules¶
Reference: TypeScript for JavaScript Programmers — "Types by Inference" and Everyday Types
The docs emphasize that you rarely need explicit annotations because TypeScript infers types.
From the docs: "TypeScript knows the JavaScript language and will generate types for you in many cases."
Inference from initialization¶
let count = 5; // inferred: number
const title = "Hi"; // inferred: "Hi" (literal type, because const)
let active = true; // inferred: boolean
let list = [1, 2, 3]; // inferred: number[]
let vs const widening¶
The docs note that const infers the literal type (narrowest), while let widens to the base type because the binding is mutable.
Contextual typing¶
When the surrounding context determines a type, parameters are inferred without annotation.
// `n` is inferred as `number` from Array<number>.map's signature
[1, 2, 3].map((n) => n.toFixed(2));
Best-practice from the Handbook¶
Annotate function boundaries (parameters and, optionally, return types) where inference cannot help; let inference handle locals. Over-annotating locals is explicitly discouraged in the "Everyday Types" examples.
9. any, unknown, and the Escape Hatches¶
Reference: Everyday Types —
anyandunknown
any — opt out of checking¶
From the docs: "When a value is of type
any, you can access any properties of it... and TypeScript won't check any of it."
let obj: any = { x: 0 };
obj.foo.bar.baz; // no error
obj(); // no error
obj = "now a string"; // no error
any propagates: anything derived from an any value is also any. The docs warn this can silently disable type safety across large swaths of code. Use noImplicitAny to forbid implicit any, and avoid explicit any.
unknown — the safe top type¶
From the docs: "
unknownis the type-safe counterpart ofany. Anything is assignable tounknown, butunknownisn't assignable to anything but itself andanywithout a type assertion or narrowing."
let value: unknown = JSON.parse("{}");
// value.foo; // Error: 'value' is of type 'unknown'.
if (typeof value === "object" && value !== null && "foo" in value) {
// narrowed — now safe
}
Type assertions (as) — "trust me"¶
From the docs: "TypeScript only allows type assertions which convert to a more specific or less specific version of a type."
const el = document.getElementById("main") as HTMLCanvasElement;
// `as` is erased at runtime — it performs NO runtime check.
The docs explicitly warn that as is a compile-time-only escape hatch; it cannot fail at runtime and therefore cannot protect you from wrong data.
10. Soundness — Where the Type System Is Deliberately Unsafe¶
Reference: TypeScript Design Goals — Non-goals
A key part of the TS↔JS relationship: TypeScript's type system is intentionally not sound. The Design Goals state a non-goal: "Apply a sound or 'provably correct' type system. Instead, strike a balance between correctness and productivity."
Documented unsoundness examples¶
// 1. Array bounds are not checked (without noUncheckedIndexedAccess)
const arr: number[] = [1, 2, 3];
const x = arr[99]; // typed `number`, actually `undefined` at runtime
// 2. Type assertions bypass checks
const n = "5" as unknown as number; // lies; runtime value is a string
// 3. `any` infects everything
const data: any = getData();
const len: number = data.length; // no check
declare function getData(): any;
The docs frame this as a productivity trade-off: a fully sound system (like some research languages) would reject too much idiomatic JavaScript. TypeScript chooses to catch the common errors while staying ergonomic for real JS code.
11. Official Examples¶
Example A — the "5-minute" migration (from the docs)¶
// Start: plain JS function
function compact(arr) {
if (arr.length > 10) return arr.trim(0, 10);
return arr;
}
// TS immediately flags: Property 'trim' does not exist on type 'any[]'.
// (the author meant `slice`) — a real bug caught at compile time.
Example B — interface describing JS object shape¶
Example C — union types from the Handbook¶
function printId(id: number | string) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}
Example D — proving erasure on the Playground¶
// Paste in https://www.typescriptlang.org/play and click the ".JS" tab:
type Color = "red" | "green" | "blue";
const favorite: Color = "red";
// The JS tab shows only: const favorite = "red";
// The `type Color` line is gone entirely.
12. Edge Cases from the Docs¶
Edge case 1: tsc emits JS even with type errors¶
Fix with "noEmitOnError": true. This is a frequent surprise for newcomers who expect a failing type-check to block output like a compiler for a sound language would.
Edge case 2: enum is NOT erased¶
enum Direction { Up, Down }
// Emits a real runtime object:
// var Direction; (function (Direction){ Direction[Direction["Up"]=0]="Up"; ... })(Direction||(Direction={}));
Unlike interface/type, a regular enum has a runtime footprint. The docs note const enum (inlined) or union-of-literals as zero-cost alternatives.
Edge case 3: declaration merging (interfaces only)¶
interface Box { height: number; }
interface Box { width: number; }
// Box now has BOTH height and width — interfaces merge; type aliases do not.
Edge case 4: structural compatibility surprises¶
interface Empty {}
const x: Empty = 42; // OK! number satisfies an empty interface structurally
const y: Empty = "s"; // also OK
An empty interface is satisfied by almost any non-null value — a classic structural-typing gotcha the docs caution against.
Edge case 5: object vs {} vs any¶
let a: object = { k: 1 }; // any non-primitive
let b: {} = 1; // almost anything except null/undefined
let c: any = anything; // disables checks
declare const anything: unknown;
13. Version and History Notes¶
| Version / Year | Milestone relevant to TS↔JS |
|---|---|
| 2012 (TS 0.8) | First public release — "typed superset of JS" framing established |
| TS 1.0 (2014) | Stable; --noImplicitAny |
| TS 2.0 (2016) | strictNullChecks, non-nullable types, unknown foundation |
| TS 3.0 (2018) | unknown type added — the safe any |
| TS 3.7 | Optional chaining ?., nullish coalescing ?? (mirror JS proposals) |
| TS 4.x | satisfies operator (4.9), better inference, as const improvements |
| TS 5.0 (2023) | moduleResolution: bundler, decorators aligned to TC39 |
| TS 5.x | verbatimModuleSyntax, isolated declarations — keep tsc and bundlers in sync |
The constant through every release: types remain fully erasable, preserving the zero-runtime-overhead guarantee.
14. Compliance Checklist¶
A correct mental model of the TS↔JS relationship per the official docs:
- I understand every valid JS program is valid TS syntax (the superset claim).
- I know type checking happens at compile time, runtime checking does not change.
- I can state that
interface,type, annotations, and generics are fully erased. - I know which constructs DO emit runtime code (
enum,class,namespace). - I understand structural typing: shape over name, no
implementsrequired. - I know excess-property checks fire only on direct object literals.
- I can explain why TypeScript adds zero runtime overhead.
- I know the type system is intentionally unsound for productivity.
- I distinguish
any(unsafe) fromunknown(safe top type). - I know
tscemits JS even with errors unlessnoEmitOnErroris set. - I never treat a TS type as runtime validation for external data.
15. Related Documentation¶
- TypeScript for JavaScript Programmers — the canonical intro
- The Basics — static checking, non-exception failures
- Everyday Types —
any,unknown, unions, inference - Type Compatibility — structural subtyping rules
- Narrowing — runtime-level type guards
- TSConfig Reference —
target,strict,noEmitOnError - TypeScript Design Goals — erasability, structural typing, non-soundness
- TS Playground — inspect emitted JS live