TypeScript
TypeScript는 자바스크립트에서 타입을 더한, 자바스크립트의 상위 집합(Superset)이라 소개된다.
// JavaScript
function add(a, b) {
return a + b;
}
let result1 = add(10, 5);
console.log(result1); // 출력: 15
let result2 = add(10, "5");
console.log(result2); // 출력: "105"
result1의 경우 숫자 + 숫자 로 출력도 숫자인 15가 출력되었고 result2의 경우 숫자 + 문자열로 문자열 "105" 를 출력하는데,
숫자 + 문자열 형태임에도 JS는 런타임에 이 상황을 오류로 처리하지 않고 문자열 연결로 처리한다.
개발자의 의도가 저런 형태도 허용하는 것이었다면 괜찮겠지만, 일반적으로 개발자가 의도한 동작이 아닐 가능성이 높다.
그리고 주석이라도 없다면 협업을 해야하는 환경에서는 더더욱 의도한대로 동작하지 않을 수 있다.
JavaScript는 코드가 실행되기 전까지 타입을 검사하지 않는 동적 타입 언어이며 컴파일이나 빌드 단계가 없어 바로 실행 가능하다는 장점이 있는 반면, 코드가 실행되어야 비로소 add(10, "5")의 결과가 "105"라는 것을 알게되기도 한다. 만약 이러한 함수가 애플리케이션의 중요한 부분에서 사용된다면, 실제 사용자 환경에서 의도하지 않은 버그가 발생될 수 있는 것이다.
// TypeScript
function add(a: number, b: number): number {
return a + b;
}
let result1: number = add(10, 5);
console.log(result1);
let result2: number = add(10, "5");
TypeScript는 정적 타입 언어로, 함수를 정의할 때부터 *인수의 타입을 명시하고, 컴파일 단계에서 타입을 엄격하게 검사한다.
함수 add의 인수 타입은 number 이므로 result2의 경우 컴파일러가 이 라인에서 즉시 오류를 감지한다.
TS컴파일러(tsc) 또는 IDE(VS Code)가 코드를 분석하고 코드를 보는 순간 컴파일러가 기대했던 타입이 아니라면 즉시 오류를 보고하여 코드 실행 전에 오류를 파악하고 수정할 수 있게 된다.
그리고 타입스크립트 코드는 최종적으로 자바스크립트로 변환(트랜스파일)되고, 변환된 JS 코드는 타입 정보가 제거되어 런타임에서는 순수 JS로 실행되게 된다.
이렇게 개발 단계에서 정적 타입 검사로 안정성을 확보하고, 컴파일 타임 검사를 통해 런타임 에러를 사전에 방지하여 협업을 통한 대규모 개발에서 이점을 가진다.
JavaScript
- 동적 타입 언어로서 유연하지만, 런타임 오류가 잦다.
- 실행하기 전까지 타입 에러를 알 수 없다.
- 대규모 프로젝트에서 유지보수가 어렵다.
TypeScript
- 자바스크립트의 상위 집합으로, 기존 JS 코드를 100% 호환 가능
- 정적 타입 검사 (Static Type Checking) 도입으로 안정성 향상
- 함수에 의도하지 않은 타입의 인수가 전달되는 것을 막아서 잘못된 사용을 방지하여 코드 안정성을 높임
- 정적 타입 언어의 특징을 가지며 변수 타입이 컴파일 시점에 결정
- 정적 타입 언어의 이점( 오류 조기 발견, 가독성, 유지 보수성 향상 등)을 제공
- 실제 실행은 자바스크립트로 컴파일 된 후 이루어지고, 개발 과정에서 타입 검사를 통해 오류를 미리 발견 가능
컴파일 과정의 차이
- JavaScript
- 인터프리터 언어로, 코드가 실행될 때 한 줄씩 해석
- TypeScript
- TS → JS로 트랜스파일(transpile) 됨
- 컴파일 시에만 타입을 사용하고, 컴파일이 끝난 JS 런타임에는 타입 정보가 존재하지 않는다.
- 단, enum은 JS로 남음 (이 부분이 중요한 차이 포인트)
타입 스크립트는 변수나 함수의 타입을 컴파일 시점에 미리 지정함으로써, 런타임이 아닌 개발 단계에서 오류를 발견할 수 있다.
하지만 브라우저는 타입스크립트를 직접 이해하지 못하기 때문에, 실행 전에 자바스크립트로 변환하는 과정(컴파일)이 필요하다.
위의 내용을 바탕으로 type (alias), interface, enum에 대해서 알아보자.
Type (alias)
type (alias)은 모든 타입에 별칭을 붙이는 기능이다. 형태가 복잡한 타입을 하나의 이름으로 간단히 묶어 사용하게 된다.
// 기본 타입 별칭
type UserName = string;
type UserAge = number;
// 객체 형태
type User = {
name: string;
age: number;
};
// 유니언(Union)
type Status = 'pending' | 'success' | 'failed';
// 인터섹션(Intersection)
type ApiResponse = User & { status: Status };
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Request = {
url: string;
method: HttpMethod;
body?: unknown;
};
// 물론 추후에 Request가 내부에서 DTO로 사용되거나 유니언 조합으로 사용되지 않고, 확장이 될 여지가 있다면
// interface로 하는 편이 좋을 수 있다.
function send(req: Request) {
console.log(`Sending ${req.method} to ${req.url}`);
}
// 에러/결과 같은 분기 처리
type Ok<T> = { type: 'ok'; data: T };
type Err = { type: 'error'; message: string };
type Result<T> = Ok<T> | Err;
function parseUser(json: string): Result<{ id: number }> {/*...*/}
// 함수, 제네릭, 매핑 유틸
type Fn<T, R> = (arg: T) => R;
type ValueOf<T> = T[keyof T]; // 매핑 유틸
type PartialExcept<T, K extends keyof T> =
Partial<T> & Pick<T, K>; // 부분 옵셔널 + 핵심 필수
유니언 ( | ) 과 인터섹션 ( & ) 의 조합이 가능하여 복잡한 타입 표현에 강하며 원시 타입, 객체, 함수, 제네릭 모두 가능하다.
런타임에는 완전히 사라져서 성능에 부담을 주지 않는다.
런타임 상수 객체가 필요 없고, 타입 안전 + 자동 완성만 있으면 될 때, 그리고 JSON/스키마와 궁합이 좋다.
주로 사용되는 곳
- 함수 파라미터나 반환 타입 정의
- 유니언, 인터섹션 등 여러 타입을 조합할 때
- DTO, 내부 데이터 구조 정의 시
* 추가
템플릿 리터럴 타입
템플릿 리터럴 타입을 사용하면 문자열 규칙 자체를 타입으로 모델링할 수 있게된다.
// 엔티티와 액션 정의
type Entity = 'user' | 'post' | 'comment';
type Action = 'created' | 'updated' | 'deleted';
// 1) 이벤트 키 패턴: `${Entity}.${Action}`
type EventName = `${Entity}.${Action}`;
// 'user.created' | 'user.updated' | 'user.deleted' |
// 'post.created' | ... | 'comment.deleted'
// 2) 라우트 패턴: `/api/${Version}/${Resource}`
type Version = 'v1' | 'v2';
type Resource = 'users' | 'posts';
type Route = `/api/${Version}/${Resource}`; // `/api/v1/users` 같은 문자열만 허용
// 3) 권한 코드: `${Module}:${Verb}`
type Module = 'admin' | 'billing' | 'profile';
type Verb = 'read' | 'write' | 'delete';
type Permission = `${Module}:${Verb}`; // 'admin:read' 등만 허용
function emit(event: EventName, payload: unknown) {
// event가 규칙에서 벗어나면 컴파일 에러
}
emit('user.created', { id: 1 }); // OK
emit('user.create', { id: 1 }); // ❌ 타입 에러 (철자/포맷 틀림)
function routeOf(r: Route) { /* ... */ }
routeOf('/api/v2/users'); // OK
routeOf('/api/v3/users'); // ❌ 'v3'는 허용 안 됨
이벤트 키, 권한 문자열, 라우트/파일 경로와 같은 문자열 규칙을 강제하고 싶을때, 백엔드-프론트 간 약속된 문자열 포맷을 타입으로 굳혀 오타와 실수를 방지하고자 할 때 사용하면 좋다. 문자열 포맷을 컴파일 타임에 안전하게 보장하게 되는 것이다.
Interface
interface는 객체의 구조를 정의하는 데 특화되어 있다. 객체가 어떤 속성과 메서드를 가져야 하는지를 약속하는 문서 같은 존재이다.
interface User {
name: string;
age: number;
introduce(): string;
}
const user: User = {
name: '도련님',
age: 27,
introduce() {
return `안녕하세요, ${this.name}입니다.`;
},
};
// 클래스 기반 설계
interface Notifier {
notify(msg: string): Promise<void>;
}
class EmailNotifier implements Notifier {
async notify(msg: string) {/*...*/}
}
- 객체 구조 중심이며 클래스와 API 등과 합이 좋다.
- 상속 (extends)가 가능하며, 선언 병합이 지원되어 동일 이름의 Interface와 자동으로 병합 된다.
- Type (alias)와 같이 런타임에는 완전히 사라진다.
// 상속 예시
interface Person {
name: string;
}
interface Developer extends Person {
skills: string[];
}
const dev: Developer = {
name: '도련님',
skills: ['TypeScript', 'NestJS'],
};
// 선언 병합 예시
interface Window {
title: string;
}
interface Window {
version: string;
}
// 자동 병합됨
const win: Window = { title: 'ChatGPT', version: '1.0' };
주로 사용되는 곳
- 클래스 기반 설계
- 클래스, 객체 구조 정의
- 외부 공개용 API (명확한 계약 구조가 필요할 때)
- 라이브러리 타입 정의
Enum
enum은 주로 상수 집합 표현을 나타내는데 적합하며, 타입스크립트에서 유일하게 런타임에도 남는 타입이다. 컴파일 후 자바스크립트 코드에서도 객체로 존재한다. 그래서 enum은 양방향 매핑 객체로 변환되어 실제 런타임에서도 사용된다.
런타임에 남아야 하는지, 번들 크기 등의 여부에 따라 type과 enum은 구분해서 사용해야한다.
값이 런타임에서도 상수 객체로 필요하다면 enum이 좋겠고, 타입 안전과 자동 완성만 필요하고 런타임 값이 따로 필요 없다면 type이 유리할 수 있다.
enum Role {
Admin,
User,
Guest,
}
console.log(Role.Admin); // 0
console.log(Role[0]); // "Admin"
enum HttpStatus {
OK = 200,
NotFound = 404,
InternalError = 500,
}
function getErrorMessage(status: HttpStatus): string {
switch (status) {
case HttpStatus.OK:
return 'Success!';
case HttpStatus.NotFound:
return 'Not Found';
case HttpStatus.InternalError:
return 'Server Error';
}
}
as const 객체
- 런타임 값과 컴파일 타임 타입을 둘다 가지게 된다. enum 대체로 많이 사용되는 패턴이다.
- 객체와 배열의 모든 속성은 readonly 처리가 된다. 이 때 타입상 readonly일 뿐, 실제 객체는 freeze되지 않음.
export const STATUS = {
Active: 'active',
Inactive: 'inactive',
} as const;
export type Status = typeof STATUS[keyof typeof STATUS];
// 값(STATUS.Active)도 있고, 타입(Status)도 생김.
as const + satisfies (TS 4.9 +)
as const의 경우 리터럴 고정과 readonly (타입) 를 제공하지만, 값 자체가 올바른지 (오타/범위) 까지는 검사하지 않는다.
여기에 satisfies를 더하면 상수 테이블을 사전에 검증하면서 리터럴 타입도 그대로 유지할 수 있게 된다.
사용에 앞서 타입스크립트 버젼을 확인하자.
// 1) 도메인 타입 정의
type Status = 'active' | 'inactive';
// 2) 상수 테이블 정의 (+ 검증)
export const STATUS = {
Active: 'active',
Inactive: 'inactive',
} as const satisfies Record<string, Status>;
// 결과
// - STATUS.Active 의 타입은 'active' (리터럴 유지)
// - 값이 'activ'처럼 오타면 컴파일 에러 발생
export type StatusValue = typeof STATUS[keyof typeof STATUS]; // 'active' | 'inactive'
이 satisfies는 Enum 대신 런타입 값과 타입을 함께 다루고 싶을 때 사용하면 좋다.
i18n 키, 권한 코드, 환경변수 키 맵, 이벤트 이름과 같은 상수 테이블을 안전하게 관리하고 싶을 때 사용하면 좋다.
const enum
const enum 은 런타임 최적화며, 이렇게 사용하면 실제 객체를 생성하지 않고, 컴파일 시점에 값이 직접 인라인 된다.
const enum은 성능 측면에서는 유리하지만 Babel 같은 트랜스파일러 환경에서 설정에 따라 동작이 제한되기도 한다.
const enum Direction {
Up,
Down,
}
const move = Direction.Up;
// JS 결과: const move = 0;
const enum Topic {
Kafka = 'kafka',
Redis = 'redis',
}
const t = Topic.Kafka;
// JS: const t = "kafka";
'Typescript' 카테고리의 다른 글
| [js, ts] 시간 계산에 getTime()을 쓰면 좋은 이유 (0) | 2025.12.25 |
|---|---|
| [Catarie] 이틀만에 S3 Put 요청이 2천회 넘기다. (0) | 2025.09.15 |
| [TypeScript] module/ moduleResolution 에러 해결과 옵션 정리 (1) | 2025.09.01 |
| [TypeScript] TS Cheat Sheet (Type, Interface) (0) | 2025.08.19 |
| 배열 관련 메서드 (0) | 2024.10.22 |