Exploring TypeScript

Exploring TypeScript

As web applications grow larger and their complexity increases exponentially, there have been significant changes in the development tools and languages we use. At the center of this is 'TypeScript', which emerged to overcome the limitations of JavaScript, originally designed for simple dynamic scripting in past web browsers. This article will delve deep into the core concepts of TypeScript and thoroughly investigate various characteristics and precautions encountered in practice.

1. Reasons to Use TypeScript

JavaScript is one of the most widely used programming languages in the world, but its inherent flexibility can lead to severe flaws in large-scale projects. TypeScript was developed to address these shortcomings of JavaScript. The key reasons for adopting TypeScript can be summarized as follows.

1.1. Ensuring Stability Through Compile-Time Error Detection

JavaScript is a 'dynamic type language'. Since variable types are determined at runtime, it is very difficult for developers to recognize type errors while writing code. For example, even if a string is inadvertently passed to a function expecting a number, JavaScript executes the code without any warnings, resulting in breaking errors or incorrect operation results displayed on the screen.

On the other hand, TypeScript is a 'static type language', which checks the types of all variables and functions at compile (or build) time, prior to execution of the code. If an incorrect type is assigned or a required parameter is missing, the compiler immediately generates an error to notify the developer. This allows for the preemptive blocking of a significant number of potential bugs that could arise at runtime during the development phase.

1.2. Strong Developer Experience (DX) and Tool Support

When modern code editors (like VS Code) are combined with TypeScript, developer productivity increases dramatically. Since type information is clearly defined, the editor suggests a list of available methods and properties in real-time through the 'IntelliSense' feature as the developer types the code.

Moreover, without needing to remember the internal structure of objects or look up API specifications every time, hovering the mouse cursor reveals the structure, type, and documented comments of the relevant data immediately. When conducting large-scale refactoring tasks, even changing the names of variables or functions collectively allows the TypeScript system to safely track and modify all related files, dramatically reducing the fear of code changes.

1.3. Improved Code Documentation and Maintainability

One of the biggest challenges in maintaining old projects or code written by other developers is determining 'what kind of data this function receives and what it returns'. In JavaScript, this requires analyzing the entire internal logic of the function or relying on separate API documentation, which can lead to serious confusion if the documentation is not up-to-date.

In TypeScript, the code itself serves as a complete specification. Since the parameter and return types are explicitly stated in the function signatures, it is possible to intuitively understand the system's intent and data flow simply by reading the code, without needing separate text documents. This results in significant cost savings from the perspective of team collaboration and long-term maintenance.

2. TypeScript Compiler

TypeScript is a language that cannot be executed directly by browsers or Node.js, as browsers can only understand JavaScript. Therefore, a process of converting TypeScript code into JavaScript code is essential for execution, and the key tool responsible for this role is the 'TypeScript Compiler (TSC)'.

2.1. The Dual Role of the Compiler (Transpiler and Type Checker)

A traditional compiler (e.g., C, Java compiler) translates source code into binary code (Machine Code) or bytecode that the computer can understand. However, the TypeScript compiler converts one high-level language into another high-level language (JavaScript), so it is more accurately described as a 'Transpiler.' TSC performs two completely separate tasks independently.

  • 1. Type Checking: It analyzes the type definitions of the written source code to verify that there are no errors.

  • 2. Transpilation: It cleans up all type syntax (interface, type, type annotation, etc.) from TypeScript source files (.ts) and converts them into pure JavaScript files (.js).

Interestingly, these two processes do not affect each other. Even if there are type errors in the code, as long as there are no syntax errors, TSC will successfully output the JavaScript file. This is an intentional design to separate type checking and the build step for greater developer convenience.

2.2. TypeScript Compilation Flow and AST

The detailed internal pipeline structure that TSC follows to analyze the characters of a code file and create the final JavaScript consists of the following steps.

  • 1. Scanner: It reads the text from the source file and breaks it down into meaningful minimum units known as 'tokens.'

  • 2. Parser: Based on the token array, it generates an 'Abstract Syntax Tree (AST)' that represents the structure of the code in a tree form.

  • 3. Binder: It traverses the AST nodes to map which scope each variable, function, and class belongs to and what symbols they represent.

  • 4. Checker: It is the core engine that performs actual type checks using the symbols generated by the binder and the AST. It thoroughly verifies assignability, function call arguments, and more.

  • 5. Emitter: Once the type checking is complete, it removes type syntax from the AST and generates source code and source maps (.js.map) compatible with the target JavaScript version (e.g., ES5, ES6, etc.).

2.3. Analysis of Core Options in TSConfig.json

The behavior of the TypeScript compiler is precisely controlled through the TSConfig.json file located at the project root. The key options that must be understood when building practical projects are as follows:

{
  "compilerOptions": {
    "target": "ES2022", // 컴파일 후 생성될 자바스크립트 버전 지정
    "module": "CommonJS", // 모듈 시스템 결정 (CommonJS, ESNext 등)
    "lib": ["DOM", "ES2022"], // 컴파일에 포함될 런타임 환경의 내장 타입 정의
    "allowJs": true, // 자바스크립트 파일도 컴파일 대상으로 허용
    "strict": true, // 강력한 타입 검사 옵션 활성화 (추천)
    "noImplicitAny": true, // 명시적 타입이 없는 경우 any 추론 금지
    "strictNullChecks": true, // null과 undefined를 고유한 타입으로 엄격히 취급
    "outDir": "./dist", // 컴파일된 자바스크립트 파일이 저장될 경로 
    "esModuleInterop": true // CommonJS와 ES 모듈 간의 호환성 확보 
   }
 }

In particular, the 'strict': true option is an essential setting for writing large-scale, high-quality code. Enabling this activates noImplicitAny, strictNullChecks, etc., causing the TypeScript compiler to operate in the most strict and safe manner.

3. How TypeScript Performs Type Checking

The way TypeScript checks types and determines compatibility is fundamentally different from traditional object-oriented languages (like Java, C++, etc.). Understanding this unique system is necessary for writing code in a TypeScript manner.

3.1. Structural Typing

Languages like C++ and Java adopt 'Nominal Typing'. In a nominal typing system, types are only compatible when the names of the classes or their inheritance relationships match exactly. In other words, even if the internal structures match 100%, if the class names differ, they are treated as completely different types.

In contrast, TypeScript supports 'Structural Typing'. Structural typing means that what matters is not the name of the type, but whether 'that type actually has the properties and methods (structure) required' for compatibility. It can be seen as the static version of 'Duck Typing' commonly referred to in dynamic languages.

interface Point {
    x: number;
    y: number;
}

function printPosition(p: Point) {
    console.log(`위치: ${p.x}, ${p.y}`);
}

// 명시적으로 Point 인터페이스를 구현하지 않은 일반 객체
const myObj = { x: 10, y: 20, z: 30 };
printPosition(myObj); // 정상 작동!

In the above code, myObj did not explicitly declare or implement the Point interface. Moreover, it also has an additional property z. However, the printPosition function accepts myObj without any errors because myObj fully incorporates the structure of 'x and y of number type' that the Point interface requires. Thanks to this structural compatibility, TypeScript can carry forward the flexible object literal usage patterns unique to JavaScript.

3.2. Type Inference

Beginner developers who are new to TypeScript often misunderstand that they must specify types with a colon (:) for every variable declaration. However, TypeScript is equipped with a very powerful 'Type Inference' engine. Even if the developer does not explicitly declare a type, the compiler automatically analyzes the context in which the code is executed and the values being assigned to determine the most appropriate type.

let message = "안녕하세요"; // string 타입으로 자동 추론
let count = 42;            // number 타입으로 자동 추론

function add(a: number, b: number) {
    return a + b;          // 반환 타입이 number로 자동 추론
}

In the above example, the moment a string is assigned to the message variable, TypeScript permanently fixes the type of this variable to string internally. Therefore, if one tries to assign a number like message = 123;, it will raise an error. In this way, clear areas can be left to inference, while explicit type declarations are made only in complex business logic or API data interface areas, maintaining readable and clean code.

4. Runtime Errors That TypeScript Cannot Detect at Compile Time

It is a clear fact that TypeScript provides strong stability, but it is by no means an all-purpose shield. All type checks in TypeScript only function at the 'compile time' of the development environment, and as explained above, all type code evaporates the moment it is transformed into JavaScript, the actual executable file. As a result, there are serious errors that crash the system at 'runtime' when the actual user runs the app, even though it has passed the compile stage lightly.

4.1. External Data and Network Communication (API Response Data)

The most common cause of runtime errors is API communication with the backend server. Frontend developers declare and apply interfaces as follows, anticipating the structure of the data that the server will provide.

interface UserProfile {
    id: number;
    name: string;
    email: string;
}

async function fetchUser(): Promise<UserProfile> {
    const response = await fetch('/api/user/1');
    return response.json(); // 컴파일러는 이것이 무조건 UserProfile 구조라고 믿음
}

The problem arises when the server changes the data structure at runtime or returns incorrect data like { "message": "Internal Error" } due to an error. The TypeScript compiler cannot know the actual runtime state of the server at the time of the build, so it recognizes the above code as safely without any issues. However, the moment code like user.name.toUpperCase() is executed in the actual browser, it throws a fatal 'TypeError' stating that it cannot read properties of undefined and crashes the app.

4.2. Misuse of Type Assertion

TypeScript has a keyword called 'as' (type assertion) that allows developers to declare they know the type of a specific piece of data more accurately than the compiler. This is a dangerous tool that forcibly nullifies the compiler's warnings.

const rawData = &quot;특정 문자열&quot; as any;
const numberData = rawData as number; // 컴파일러는 숫자로 인정

console.log(numberData.toFixed(2)); // 런타임 에러 발생

Since the compiler acknowledges that the developer asserted 'as number', it fully treats the variable numberData as a number and allows compilation. However, since the actual data is still a string, it will crash indiscriminately at runtime. Irresponsible use of the any type or incomplete type assertions are the main culprits that undermine the reliability of the entire type system.

4.3. Array Index Access Errors (Out of Bounds)

TypeScript is designed to be generally lenient about accessing out-of-bounds indices, which makes bugs more likely to occur.

const numbers: number[] = [1, 2, 3];
const fourthNumber = numbers[5];

console.log(fourthNumber.toFixed(2)); // 런타임에 undefined 에러 발생

Since there is nothing at the 5th index of the array, the actual value extracted is undefined. However, TypeScript passes the compilation assuming that since the numbers array consists of numbers, the result of all index access will also be a number. To overcome this limitation, it is necessary to enable the noUncheckedIndexedAccess option in TSConfig.json to enforce that undefined may mix in during index access.

5. Comparison with JavaScript

The final step to fully dissect TypeScript is to make a clear quantitative and qualitative comparison with its parent, JavaScript. Understanding the differences between the two languages clearly will allow for appropriate use of tools in the right places.

Comparison Items

JavaScript (JavaScript)

TypeScript

Type System

Dynamic Type (Determined at Runtime)

Static Type (Determined at Compile Time)

Error Detection Timing

Runtime

Compile-time

Code Autocompletion

Limited (Based on Inference)

Very Powerful and Accurate (Type-based)

Tools and Ecosystem

Direct Execution in Browser/Node.js

Transformation Process Required through Compiler (TSC)

Project Suitability

Small Scripts, Rapid Prototyping

Large-scale applications, multi-person collaboration projects

5.1. Differences between paradigm and philosophy

JavaScript has a philosophy of 'be as flexible as possible, but don't stop executing first.' There is a strong tendency to handle exceptions roughly internally to ensure that the entire webpage does not stop, even with minor mistakes. In contrast, TypeScript follows the strict philosophy of 'if you are not sure, don't even execute.' Although it may incur higher initial writing costs, the belief is that strictness provides much greater benefits for the stability of large-scale systems.

6. Conclusion

In conclusion, TypeScript is not a substitute for JavaScript, but a large extension and complement. It perfectly inherits the ecosystem and standards of JavaScript while tightly integrating the safety mechanisms essential for large-scale enterprise environments.

Recognizing the limitation that runtime errors cannot be 100% prevented, if proper TSConfig options are set and combined with runtime validation libraries like Zod, you can create almost bulletproof secure code. We strongly recommend advancing the development paradigm that remained at the JavaScript level to TypeScript, to build more robust and elegant applications.

May

Site footer