본문 바로가기

타입 관련 고급 기능

1. 타입 단언과 널 처리

1.1 Non-Null 단언 연산자

JavaScript에서는 DOM 요소를 선택할 때 해당 요소가 없을 수 있어서 null을 반환할 수 있습니다. TypeScript에서는 이런 상황에서 ! 연산자를 사용하여 "이 값은 절대 null이 아닙니다"라고 컴파일러에게 알려줄 수 있습니다.

// 기본적인 예제
const $button = document.querySelector('button');
// 이 시점에서 TypeScript는 $button이 null일 수 있다고 생각합니다
// 타입스크립트는 이 경우 button을 아래와 같이 타입추론 합니다
const $button: HTMLButtonElement | null
// $button.click(); // 에러 발생!

// Non-Null 단언 사용
const $safeButton = document.querySelector('button')!;
$safeButton.click(); // 정상 작동

1.2 타입 단언(Type Assertion)

때로는 우리가 TypeScript보다 어떤 값의 타입을 더 정확히 알고 있는 경우가 있습니다. 이럴 때 as 키워드를 사용하여 타입을 명시적으로 지정할 수 있습니다. 다만 타입 단언은 매우 주의해야 하는데, 타입 단언을 할 경우 런타임과 컴파일 모두 유형 검사를 우회하게 됩니다. 따라서 타입 단언을 남발하면 타입 안전성이 떨어질 수 있습니다.

그러면 타입 단언은 언제, 왜 사용해야 할까요? 타입 단언을 사용하게 되면 사람과 컴퓨터 모두가 타입을 인식하기 좋습니다. 예를 들어, IDE 같은 경우 자동완성 기능을 제공할 때 타입 단언을 사용하면 더 정확한 정보를 제공할 수 있습니다. 사람이 읽을 때에도 타입 단언을 사용하면 코드를 더 쉽게 이해할 수 있습니다.

// 이벤트 핸들러 예제
document.querySelector('input')!.addEventListener('change', (e) => {
    // e.target은 기본적으로 EventTarget 타입
    const target = e.target as HTMLInputElement;
    console.log(target.value); // .value 접근 가능

    // as 없이 작성한다면:
    // const target = e.target;
    // console.log(target.value); // 에러 발생!
});

// 실제 사용 예제
interface User {
    name: string;
    age: number;
}

const userJSON = '{"name": "John", "age": 30}';
const user = JSON.parse(userJSON) as User;
console.log(user.name); // 타입 안전성 확보
// API 응답 데이터 예제

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

async function fetchUser(): Promise<User> {
  const response = await fetch('/api/user');
  const data = await response.json(); // data는 any 타입
  
  // API 응답이 User 타입임을 알고 있을 때
  const user = data as User;
  console.log(user.name);
  console.log(user.email);
}
// 권한 검증 예제

interface Admin {
  role: 'admin';
  permissions: string[];
}

interface User {
  role: 'user';
  level: number;
}

type Person = Admin | User;

function showInfo(person: Person): void {
  if (person.role === 'admin' && 'permissions' in person) {
    const admin = person as Admin;
    console.log(`권한: ${admin.permissions.join(', ')}`);
  } else {
    const user = person as User;
    console.log(`레벨: ${user.level}`);
  }
}

function canDelete(person: Person): boolean {
  if (person.role === 'admin' && 'permissions' in person) {
    const admin = person as Admin;
    return admin.permissions.includes('delete');
  }
  return false;
}

위 예제를 타입 가드를 활용한 코드로 리팩토링 할 수도 있습니다.

// 타입 가드 함수 정의
function isAdmin(person: Person): person is Admin {
  return person.role === 'admin' && 'permissions' in person;
}

function showInfo(person: Person): void {
  if (isAdmin(person)) {
    // person은 자동으로 Admin 타입
    console.log(`권한: ${person.permissions.join(', ')}`);
  } else {
    // person은 자동으로 User 타입
    console.log(`레벨: ${person.level}`);
  }
}

function canDelete(person: Person): boolean {
  if (isAdmin(person)) {
    return person.permissions.includes('delete');
  }
  return false;
}

💡 주의사항: 타입 단언은 필요한 경우에만 사용해야 합니다. 과도한 사용은 타입 안전성을 해칠 수 있습니다.

타입 단언 vs. 타입 가드

방법장점단점타입 검증 여부
타입 단언(as)간단하고 빠름런타임 안전성 보장 안 됨타입 검증을 우회함
타입 가드재사용 가능, 명확함추가 함수 작성 필요실제로 타입을 검증함

2. 객체 타입 고급 기능

2.1 인덱스 시그니처

객체의 속성 이름을 미리 알 수 없을 때 유용한 기능입니다. 주로 동적 데이터를 다룰 때 사용됩니다.

// 기본 예제
interface Dictionary {
    [key: string]: string;
}

const colors: Dictionary = {
    red: "#ff0000",
    blue: "#0000ff",
    green: "#00ff00"
};

// 실제 활용 예제
interface UserData {
    id: number;
    name: string;
    [key: string]: string | number | boolean; // 다양한 타입 허용
}

const userData: UserData = {
    id: 1,
    name: "Alice",
    age: 25,
    isAdmin: true,
    department: "Engineering"
};

❗️주의❗️ 고정 속성의 타입은 인덱스 시그니처 타입과 호환되어야 합니다.

interface Wrong {
  name: string;
  age: number;
  [key: string]: number;  // 오류! name은 string인데 number여야 함
}

interface Correct {
  name: string;
  age: number;
  [key: string]: string | number;  // OK
}

인덱스 시그니처는 동적인 속성 이름을 처리할 수 있고 유연한 객체 구조를 정의할 수 있다는 장점이 있지만, 타입 안정성이 약해지고 오타를 잡아내기 어렵다는 단점이 있습니다.

인덱스 시그니처의 대안: Record 유틸리티 타입

Record<Keys, Type>은 키와 값의 타입을 명확하게 지정할 수 있는 유틸리티 타입입니다.

// 기본 예제

type ScoresRecord = Record<string, number>;

const scores: ScoresRecord = {
  Alice: 95,
  Bob: 87,
  Charlie: 92
};

Record의 강력한 장점은 특정 키를 지정할 수 있다는 점입니다.

// 권한 시스템 예제

type UserRole = 'admin' | 'editor' | 'viewer';

type RolePermissions = Record<UserRole, string[]>;

const permissions: RolePermissions = {
  admin: ['read', 'write', 'delete'],
  editor: ['read', 'write'],
  viewer: ['read']
  // 세 가지 역할 모두 정의해야 함!
};
// 다국어 번역 예제

type Language = 'ko' | 'en' | 'ja';

type Translations = Record<Language, {
  greeting: string;
  farewell: string;
}>;

const messages: Translations = {
  ko: {
    greeting: '안녕하세요',
    farewell: '안녕히 가세요'
  },
  en: {
    greeting: 'Hello',
    farewell: 'Goodbye'
  },
  ja: {
    greeting: 'こんにちは',
    farewell: 'さようなら'
  }
  // 세 언어 모두 필수!
};

2.2 keyof 연산자

객체의 모든 키를 타입으로 추출할 때 사용합니다. 특히 제네릭과 함께 사용할 때 유용합니다. 이 연산자를 사용하면 객체의 키를 문자열로 추출할 수 있습니다. 간단한 예제부터 살펴보도록 하겠습니다.

interface Product {
    id: number;
    name: string;
    price: number;
    category: string;
}
 
// Product의 모든 키를 타입으로 추출
type ProductKeys = keyof Product; // "id" | "name" | "price" | "category"

const data: ProductKeys = "id"; 
// 여기에 "name", "price", "category" 등 다른 값을 사용할 수 있습니다.
// 다만 1000, "hello" 등의 key가 아닌 경우에는 사용할 수 없습니다.

console.log(data); // id

이는 아래와 같은 방식으로 사용할 수 있게 해줍니다.

interface Product {
    id: number;
    name: string;
    price: number;
    category: string;
}

// Product의 모든 키를 타입으로 추출
type ProductKeys = keyof Product; // "id" | "name" | "price" | "category"

// 실제 활용 예제
function getProductValue(product: Product, key: keyof Product) {
    return product[key];
}

const laptop: Product = {
    id: 1,
    name: "노트북",
    price: 1000000,
    category: "전자기기"
};

console.log(getProductValue(laptop, "name")); // "노트북"
console.log(getProductValue(laptop, "invalid")); // 컴파일 에러!

다만 이렇게 사용했을 경우 일반적으로 laptop["name"]과 같이 직접 접근하는 것이 더 간단할 수 있습니다. 하지만 keyof 연산자는 복잡한 타입에서 사용할 때 유용합니다.

3. 연습문제

  1. Non-Null 단언 문제

    // TODO: Non-Null 단언을 사용하여 아래 코드를 완성하세요.
    const $input = document.querySelector('input');
    // $input의 value를 콘솔에 출력하세요.
    
  2. 타입 단언 문제

    // TODO: 아래 JSON 데이터를 파싱하고 타입 단언을 사용하여
    // name과 age를 안전하게 접근할 수 있도록 만드세요.
    const data = '{"name": "Jane", "age": 25}';
    
  3. 인덱스 시그니처 문제

    // TODO: 학생의 과목별 성적을 저장할 수 있는 인터페이스를 만드세요
    // 학생의 이름은 필수이며, 나머지 과목들은 동적으로 추가될 수 있습니다
    
  4. Record 유틸리티 타입 문제

    // TODO: 카페의 요일별 영업시간을 관리하는 타입과 객체를 작성하세요.
    // 요일: '월' | '화' | '수' | '목' | '금' | '토' | '일'
    // 각 요일마다 open(오픈 시간)과 close(마감 시간) 속성 필요
    // 모든 요일의 영업시간이 반드시 정의되어야 함
    
    type Weekday = // ???
    type BusinessHours = // Record를 사용하세요
    
    const cafeHours: BusinessHours = {
        // 작성하세요
    };
    
  5. keyof 연산자 문제

    interface Car {
        brand: string;
        model: string;
        year: number;
    }
    // TODO: Car 인터페이스의 속성 중 하나를 인자로 받아
    // 해당 속성의 값을 반환하는 함수를 작성하세요.
    

4. 연습문제 해답

  1. Non-Null 단언 해답

    const $input = document.querySelector('input')!;
    console.log($input.value);
    
  2. 타입 단언 해답

    interface Person {
        name: string;
        age: number;
    }
    const data = '{"name": "Jane", "age": 25}';
    const person = JSON.parse(data) as Person;
    console.log(person.name, person.age);
    
  3. 인덱스 시그니처 해답

    interface StudentGrades {
        name: string;
        [subject: string]: string | number; // 과목명: 점수
    }
    
    const studentGrades: StudentGrades = {
        name: "김철수",
        math: 95,
        english: 88,
        science: 92
    };
    
  4. Record 유틸리티 타입 해답

    // 타입 정의
    type Weekday = '월' | '화' | '수' | '목' | '금' | '토' | '일';
    
    type BusinessHours = Record<Weekday, {
        open: string;
        close: string;
    }>;
    
    // 객체 작성
    const cafeHours: BusinessHours = {
        월: { open: '09:00', close: '22:00' },
        화: { open: '09:00', close: '22:00' },
        수: { open: '09:00', close: '22:00' },
        목: { open: '09:00', close: '22:00' },
        금: { open: '09:00', close: '23:00' },
        토: { open: '10:00', close: '23:00' },
        일: { open: '10:00', close: '21:00' }
    };
    
    // 사용 예시
    console.log(cafeHours.월);  // { open: '09:00', close: '22:00' }
    console.log(cafeHours.토.open);  // '10:00'
    
  5. keyof 연산자 해답

    interface Car {
        brand: string;
        model: string;
        year: number;
    }
    
    function getCarProperty(car: Car, property: keyof Car) {
        return car[property];
    }
    
    const myCar: Car = {
        brand: "Cats Motors",
        model: "Licat1",
        year: 2224
    };
    
    console.log(getCarProperty(myCar, "brand")); // "Cats Motors"
    console.log(myCar['brand']); // "Cats Motors"
    // 간단한 타입의 경우에는 myCar['brand']와 같이 직접 접근해도 크게 문제가 없지만
    // 복잡한 타입의 경우에는 keyof를 사용하는 것이 안전합니다.
    
타입 관련 고급 기능 - TypeScript 베이스캠프 | 위니버시티