thumbnail

TypeScript에서 satisfies와 as const로 리터럴 타입 안전하게 다루기

생성일2024. 8. 29.
태그
작성자지한솔

notion image
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 타입의 categorystring으로 정의되어 있습니다. 따라서 products 배열의 category 필드는 각각 "Electronics", "Clothing", "Grocery"와 같은 값으로 초기화되지만, TypeScript는 이를 넓은 string 타입으로 추론합니다. 결과적으로, CategoryTypestring으로 추론되어, 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'로 정확히 추론됩니다.
여기서 initialStateState 타입을 만족하도록 강제하면서도, 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에서 satisfiesas const 키워드를 사용하여 리터럴 타입을 정확하게 추론하고 타입 안전성을 유지하는 방법을 살펴보았습니다.
  • satisfies: 객체가 특정 타입을 만족하면서도 리터럴 타입을 유지하도록 도와줍니다.
  • as const: 배열이나 객체의 값을 리터럴 타입으로 고정하여 타입 추론에서 더 구체적인 타입을 유지할 수 있습니다.
이 도구들을 활용하여 TypeScript에서 더욱 안전하고 유지보수하기 쉬운 코드를 작성해보세요.