[ TypeScript ] Type Inference

Lutica_·2025년 8월 13일
0

Type Inference란 무엇인가?

  • Type Inference는 타입 추론이라고 이야기한다.
  • 타입추론에 관한 공식 문서는 여기있다.

중요 Keyword

  • never : 절대 이 객체의 프로퍼티를 할당 할 수 없다는 의미이다.
  • unknown : 타입 축소를 제하고, any이외의 객체에 대입 불가능하다.
  • any : 모든 객체가 될 수 있다는 의미의 type이다. 왠만하면 안쓰는것을 권장하나, 쓴다면 특정한 이유와 함께 쓰길 바란다. 이것만 쓸거면 JS쓰는게 나을지도

특별한 동작

  • 타입축소 : Narrowing, 말 그대로 확장되어있는 타입에서 타입을 줄여주는 것이다.

사용 예제

  • 아래는 이벤트 객체에 따라 객체를 만드는 예제이다. Narrowing을 활용한다.
/**
 * @fileoverview never와 조건부 타입을 활용한 타입-안전 이벤트 객체 생성기
 */

// --- 1단계: 이벤트별 페이로드 타입 정의 ---
// 각 이벤트 이름('LOGIN', 'PURCHASE' 등)을 키(key)로,
// 해당 이벤트가 가져야 할 페이로드의 구조를 값(value)으로 하는
// '타입 맵(Type Map)'을 정의합니다.
type EventPayloads = {
  LOGIN: { userId: string };
  PURCHASE: { productId: string; price: number };
  LOGOUT: {}; // LOGOUT 이벤트는 페이로드가 비어있습니다.
};

// EventPayloads의 모든 키를 유니온 타입으로 추출합니다.
// 결과: 'LOGIN' | 'PURCHASE' | 'LOGOUT'
type EventType = keyof EventPayloads;


// --- 2단계: `never`를 활용한 조건부 타입 함수 ---
// 이 코드의 핵심입니다! 제네릭을 사용하여 유연성과 타입 안정성을 모두 잡습니다.
// T: 이벤트 이름 (e.g., 'LOGIN')
// P: 함수에 전달된 페이로드 객체의 실제 타입
/**
 * 전달된 페이로드(P)가 이벤트(T)에 적합한지 검사하는 엄격한 함수.
 * @returns 조건이 참이면 정상적인 이벤트 객체를, 거짓이면 'never' 타입을 반환합니다.
 */
function createStrictEvent<T extends EventType, P>(
  type: T,
  payload: P
  // ✨ 여기가 바로 마법이 일어나는 곳입니다! ✨
): P extends EventPayloads[T] 
	? { type: T; payload: P } : never { //<- `?`는 조건에 근거하여 타입을 변환한다.
  // P가 EventPayloads[T]에 할당 가능한 타입인가?
  // YES -> { type: T, payload: P } 타입을 반환
  // NO  -> never 타입을 반환 (이 타입은 절대 발생해선 안 됨)

  // 이 함수의 타입 안정성은 함수를 '호출하는 쪽(*위의 함수 return을 의미.*)'에서 보장받으므로,
  // 내부 구현에서는 타입 단언을 사용할 수 있습니다.
  return { type, payload } as any; // <- any를 쓰는것이 이상하다면, 대치하여도 좋다.
}


// --- 3단계: 실제 사용 예시 및 타입 검증 ---

// ✅ 올바른 사용 예시
// 'LOGIN' 이벤트와 그에 맞는 페이로드({ userId: string })를 전달했습니다.
// 'validEvent'의 타입은 `{ type: "LOGIN"; payload: { userId: string; }; }` 로 완벽하게 추론됩니다.
const validEvent = createStrictEvent('LOGIN', { userId: 'user-123' });
console.log(validEvent.payload.userId); // 'user-123' (당연히 정상입니다)

// ❌ 잘못된 사용 예시 1: 속성 이름이 틀림
// 'LOGIN' 이벤트에 엉뚱한 페이로드를 전달했습니다.
// 조건부 타입이 실패하여 'invalidEvent1'의 타입은 'never'로 추론됩니다.
const invalidEvent1 = createStrictEvent('LOGIN', { productId: 'item-abc' }); 
// <- 왜냐하면, EventPayloads에   LOGIN: { userId: string }; 로 정의되어 있기 때문이다.

// 따라서 `invalidEvent1`을 사용하려고 하면 바로 오류가 발생합니다.
// console.log(invalidEvent1.payload); // ❌ 컴파일 에러: "'never' 형식에 'payload' 속성이 없습니다."


// ❌ 잘못된 사용 예시 2: 속성 타입이 틀림
// 'PURCHASE' 이벤트의 price가 number여야 하는데 string을 전달했습니다.
// 'invalidEvent2'의 타입 역시 'never'로 추론됩니다.
const invalidEvent2 = createStrictEvent('PURCHASE', {
  productId: 'prod-456',
  price: '15,000원', // price는 number여야 합니다!
});

// `never` 타입의 변수는 그 어디에도 할당할 수 없어 실수를 원천적으로 방지합니다.
// let someVariable = invalidEvent2; // ❌ 컴파일 에러: "'never' 형식은 '...' 형식에 할당할 수 없습니다."

Discriminated Unions

  • 위의 사례는 그러나, EventPayloads에 의존하는 코드의 의존성으로 인하여 가독성이 불완전하다.
  • 아래 대안은 조금 더 일반화하여, OR type에 대한 가독성 있는 타입 추론을 제공한다.
// 1. 각 이벤트 객체의 형태를 개별 인터페이스로 정의합니다.
// 공통적으로 'type'이라는 리터럴 속성을 갖는 것이 핵심입니다.
interface LoginEvent {
    type: 'LOGIN';
    payload: { userId: string };
}

interface PurchaseEvent {
    type: 'PURCHASE';
    payload: { productId: string; price: number };
}

// 2. 모든 이벤트 타입을 하나의 유니온 타입으로 묶습니다.
type AppEvent = LoginEvent | PurchaseEvent;

// 3. 이벤트를 처리하는 함수
function handleEvent(event: AppEvent) {
    // 'event.type'의 값을 기준으로 타입을 좁힙니다.
    switch (event.type) {
        case 'LOGIN':
            // 이 블록 안에서 event는 'LoginEvent' 타입으로 확정됩니다.
            console.log('User logged in:', event.payload.userId);
            break;
        
        case 'PURCHASE':
            // 이 블록 안에서 event는 'PurchaseEvent' 타입으로 확정됩니다.
            console.log('Product purchased:', event.payload.productId, 'at', event.payload.price);
            break;
        
        default:
            // 모든 경우를 처리했다면 이곳은 실행될 수 없는 'never' 상태가 됩니다.
            const exhaustiveCheck: never = event;
            return exhaustiveCheck;
    }
}

// ✅ 올바른 사용
handleEvent({ type: 'LOGIN', payload: { userId: 'user-555' }});

// ❌ 잘못된 사용 (컴파일 에러 발생)
// handleEvent({ type: 'LOGIN', payload: { price: 999 }});
profile
해보고 싶고, 하고 싶은 걸 하는 사람

0개의 댓글