TypeScript's static typing system is built on top of the JavaScript runtime as an erasable layer (Type Erasure). All types, interfaces, and annotations exist exclusively during compilation in the AST (Abstract Syntax Tree) and are completely removed during transpilation to pure JavaScript. This means zero runtime overhead: types do not increase bundle size or slow down program execution.
The foundation of the system consists of primitive types: string, number, boolean, bigint, symbol, null, and undefined. Unlike JavaScript's object wrappers for primitives (String, Number, Boolean), primitive types in TypeScript are strictly written in lowercase and represent indivisible atomic values.
A key mechanism for TypeScript's ergonomics is automatic type inference. The compiler uses contextual control flow analysis and a unification algorithm to automatically determine the narrowest safe type for a variable without the need for excessive manual annotation.
In TypeScript, there is a fundamental distinction between mutable variables (let, var) and constants (const). This distinction directly determines the compiler's inference strategy:
+-------------------------------------------------------------------------+
| MENTAL MODEL OF TYPE INFERENCE |
+-------------------------------------------------------------------------+
| let status = 'pending' ---> Type: string (Type Widening) |
| (Can change in the future) Any string can be assigned |
+-------------------------------------------------------------------------+
| const status = 'pending' ---> Type: 'pending' (Literal Type) |
| (Never changes) Narrow string subtype of a single word |
+-------------------------------------------------------------------------+
| const config = { port: 3000 } ---> Type: { port: number } |
| (Object is constant by reference, Properties are mutable! |
| but fields can be reassigned) "as const" is needed for deep immutability |
+-------------------------------------------------------------------------+as const)
The parser transforms the source code into an AST. The initialization expression is linked to the variable identifier.
The compiler analyzes the declaration modifier: for `const`, a literal type is inferred; for `let`, Type Widening is applied.
In subsequent operations, the compiler compares the type of the new value with the inferred type according to structural compatibility rules.
The compiler's emitter removes all annotations and generates pure JS code without any type checking polyfills.
What type will the `theme` variable receive in the following expression: `const theme = 'dark'`?
'GET' | 'POST' | 'PUT' | 'DELETE' instead of a broad string type.variant: 'primary' | 'secondary' | 'ghost').symbol and bigint for high-precision financial transactions and unique global keys.as const, eliminating typos in URL segments at compile time.In JavaScript, there are primitives ('hello', 42, true) and their corresponding object wrappers (new String('hello')). In TypeScript, using uppercase types like String, Number, Boolean, Object is considered a serious error:
// CORRECT: using primitive types
const username: string = 'alex_dev';
const retryCount: number = 3;
const isProduction: boolean = true;
const transactionId: bigint = 9007199254740991n;
const uniqueKey: symbol = Symbol('node_id');
// INCORRECT: wrapper types describe class instances, not primitive values!
// let badString: String = 'bad'; // Anti-pattern!The compiler automatically narrows the type when using const and widens it when using let:
let userRole = 'admin'; // Type: string (can reassign userRole = 'viewer')
const defaultRole = 'admin'; // Type: 'admin' (literal type)
function assignPermission(role: 'admin' | 'moderator') {
console.log(`Access granted for ${role}`);
}
assignPermission(defaultRole); // Correct: 'admin' is compatible with 'admin' | 'moderator'
// COMPILATION ERROR:
// assignPermission(userRole);
// TS2345: Argument of type 'string' is not assignable to parameter of type ''admin' | 'moderator''.as const#By default, object properties undergo widening, even if the object itself is declared as const:
// Without 'as const': properties are mutable
const defaultSettings = {
endpoint: 'https://api.wpskills.dev/v1',
timeout: 5000,
logLevel: 'info', // Field type: string!
};
// defaultSettings.logLevel = 'debug'; // Allowed by the compiler!
// With 'as const': deep readonly and literal types
const safeSettings = {
endpoint: 'https://api.wpskills.dev/v1',
timeout: 5000,
logLevel: 'info',
} as const;
// Type of safeSettings:
// {
// readonly endpoint: "https://api.wpskills.dev/v1";
// readonly timeout: 5000;
// readonly logLevel: "info";
// }
// safeSettings.timeout = 10000; // ERROR TS2540: Cannot assign to 'timeout' because it is a read-only property.Why does the compiler throw an error when passing `config.method` to the `fetchData(config.url, config.method)` function, if `method: 'POST'` is inside the object `const config = { url: '/api', method: 'POST' }`?
Uppercase types String, Number, Boolean describe instances of built-in JS constructors, not primitives. A primitive value is not a full-fledged Object instance, which leads to unexpected compatibility errors:
// ERROR:
function format(id: String) { /* ... */ }
// CORRECT:
function format(id: string) { /* ... */ }Always use only lowercase primitive names.
Writing let count: number = 0 or const name: string = 'Alice' is code noise. The compiler correctly infers the types of trivial expressions. Annotate types only where inference is not possible (function parameters, empty arrays, complex return types).
Declaring a variable as any disables the compiler's type checking for the entire downstream flow of operations:
// DANGEROUS: compiler is silent, application crashes at runtime!
const rawPayload: any = JSON.parse('{"status": 200}');
rawPayload.unknownMethod(); // TypeError at runtime!
// CORRECT: use unknown with subsequent validation
const safePayload: unknown = JSON.parse('{"status": 200}');