
TypeScript를 사용하면 정적 타입 시스템을 통해 코드의 안전성을 보장할 수 있습니다. 그러나 때로는 우리가 의도한 것보다 더 넓은 타입으로 추론되어, 예상치 못한 타입 오류가 발생할 수 있습니다.
이번 포스팅에서는 TypeScript 4.9에 도입된
satisfies
키워드와 as const
키워드를 활용하여 리터럴 타입을 안전하게 다루는 방법을 알아보겠습니다. 이를 통해 타입 안전성을 유지하고 코드의 오류를 줄일 수 있습니다.문제 상황
먼저, 다음과 같은 코드 예제를 살펴봅시다:
type Product = { id: string; name: string; price: number; category: string; // category 타입이 여전히 string으로 정의됨 }; const products = [ { id: '1', name: 'Laptop', price: 1500, category: 'Electronics' }, { id: '2', name: 'T-Shirt', price: 20, category: 'Clothing' }, { id: '3', name: 'Apple', price: 1, category: 'Grocery' }, ] satisfies Readonly<Product[]>; // type CategoryType = string type CategoryType = (typeof products)[number]['category']; const selectedCategory: CategoryType = 'Furniture'; // 오류가 발생하지 않음
위 코드에서
Product
타입의 category
는 string
으로 정의되어 있습니다. 따라서 products
배열의 category
필드는 각각 "Electronics"
, "Clothing"
, "Grocery"
와 같은 값으로 초기화되지만, TypeScript는 이를 넓은 string
타입으로 추론합니다. 결과적으로, CategoryType
도 string
으로 추론되어, selectedCategory
에 "Furniture"
와 같은 값이 들어가도 오류가 발생하지 않습니다.문제 해결: 리터럴 타입 유지하기
category
필드를 넓은 string
타입으로 정의하면서도, 리터럴 타입을 유지하고 타입 안전성을 보장하려면 어떻게 해야 할까요? 이를 해결하기 위해 satisfies
키워드와 as const
를 사용할 수 있습니다.해결 방법 1: Product
타입의 category
를 리터럴 타입으로 정의
Product
타입의 category
필드를 구체적인 리터럴 타입으로 정의하여, 코드가 더 안전하게 동작하도록 만들 수 있습니다:type Product = { id: string; name: string; price: number; category: 'Electronics' | 'Clothing' | 'Grocery'; // category를 리터럴 타입으로 정의 }; const products: Readonly<Product[]> = [ { id: '1', name: 'Laptop', price: 1500, category: 'Electronics' }, { id: '2', name: 'T-Shirt', price: 20, category: 'Clothing' }, { id: '3', name: 'Apple', price: 1, category: 'Grocery' }, ]; // type CategoryType = 'Electronics' | 'Clothing' | 'Grocery' type CategoryType = (typeof products)[number]['category']; const selectedCategory: CategoryType = 'Furniture'; // 오류 발생
이 코드에서는
Product
타입에서 category
필드를 리터럴 타입으로 정의했기 때문에, CategoryType
이 'Electronics' | 'Clothing' | 'Grocery'
로 정확히 추론됩니다. 결과적으로, 잘못된 값인 'Furniture'
를 할당하려고 하면 TypeScript는 오류를 발생시킵니다.해결 방법 2: as const
키워드를 사용하여 리터럴 타입 유지
또 다른 방법으로,
as const
키워드를 사용하여 products
배열을 리터럴 타입으로 고정할 수 있습니다:type Product = { id: string; name: string; price: number; category: string; // category는 여전히 string으로 정의됨 }; const products = [ { id: '1', name: 'Laptop', price: 1500, category: 'Electronics' }, { id: '2', name: 'T-Shirt', price: 20, category: 'Clothing' }, { id: '3', name: 'Apple', price: 1, category: 'Grocery' }, ] as const satisfies Readonly<Product[]>; // type CategoryType = 'Electronics' | 'Clothing' | 'Grocery' type CategoryType = (typeof products)[number]['category']; const selectedCategory: CategoryType = 'Furniture'; // 오류 발생
여기서
as const
키워드를 사용하면 products
배열의 각 항목이 리터럴 타입으로 고정됩니다. 결과적으로, CategoryType
은 'Electronics' | 'Clothing' | 'Grocery'
로 정확하게 추론되며, 잘못된 값인 'Furniture'
를 할당하려는 시도에서 오류가 발생합니다.좀 더 나은 해결 방법은?
해결 방법 1
에서는 다음과 같은 문제가 발생할 수 있습니다.기획자: Category에 음식을 추가해주시요.
라는 요구사항이 들어온다면, products에
category: ‘Food’
를 넣으면 에러가 발생합니다. 왜? 저희는 Product
에 리터널 타입으로 카테고리를 정의했기 때문이죠.하지만 해결 방법 2의 경우 유연하게 가져갈수 있습니다. 새로운 products에
‘Food’
카테고리를 추가한다면 자연스럽게 CategoryType
에 ‘Food’ 도 들어가게 됩니다.// type CategoryType = 'Electronics' | 'Clothing' | 'Grocery' | 'Food' type CategoryType = (typeof products)[number]['category'];
상황에 따라 다를 수 있지만 유동적으로 변하는 환경이라면 해결 방안 2안이 조금 더 현명한 방법이 될 수 있습니다.
satisfies
의 추가적인 활용 예제
satisfies
키워드를 사용하면 더 복잡한 타입 구조에서도 타입 안전성을 유지할 수 있습니다. 몇 가지 추가적인 활용 예제를 살펴보겠습니다.예제 1: 상태 관리에서의 satisfies
사용
상태 관리에서
satisfies
를 사용하여 상태 객체의 타입을 안전하게 정의할 수 있습니다:type State = { id: number; status: 'loading' | 'success' | 'error'; data: unknown; }; const initialState = { id: 1, status: 'loading', data: null, } satisfies State; // state.status는 'loading' | 'success' | 'error'로 정확히 추론됩니다.
여기서
initialState
가 State
타입을 만족하도록 강제하면서도, status
의 리터럴 타입이 유지됩니다.예제 2: API 응답 데이터 처리
API 응답 데이터를 처리할 때
satisfies
를 사용하여 예상되는 데이터 구조를 강제할 수 있습니다:type ApiResponse = { success: boolean; message: string; data: any; }; const response = { success: true, message: 'Data fetched successfully', data: { id: 1, name: 'John Doe' }, } satisfies ApiResponse; // response.data는 any가 아닌 { id: number; name: string; }로 정확히 추론됩니다.
이 예제에서
satisfies
를 사용하면 response
객체가 ApiResponse
타입을 만족하면서도, data
필드가 리터럴 타입으로 정확하게 추론됩니다.이 포스팅에서는 TypeScript에서
satisfies
와 as const
키워드를 사용하여 리터럴 타입을 정확하게 추론하고 타입 안전성을 유지하는 방법을 살펴보았습니다.satisfies
: 객체가 특정 타입을 만족하면서도 리터럴 타입을 유지하도록 도와줍니다.
as const
: 배열이나 객체의 값을 리터럴 타입으로 고정하여 타입 추론에서 더 구체적인 타입을 유지할 수 있습니다.
이 도구들을 활용하여 TypeScript에서 더욱 안전하고 유지보수하기 쉬운 코드를 작성해보세요.
satisfies
: TypeScript 4.9 문서