📙

8장 타입 말해주기

8.1 타입 단언 (Type Assertion)

8.1.1 타입 단언이란?

타입 단언은 타입스크립트가 타입 추론을 예상과 다르게 추론했을 때 사용합니다. 개발자가 해당 값의 타입을 확신할 때 명시적으로 지정해 컴파일러에게 “이 값은 이 타입이야” 라고 알려주는 것과 같습니다. 대표적인 타입 단언 방법 두 가지를 알아보겠습니다.

1) as 단언

기본적인 as 연산자를 사용해 Value as <Type> 형식으로 타입 단언을 할 수 있습니다.
let one: unknown = 'hello'; let oneLength : number = ((one as string).length); console.log(oneLength); // 5
위의 코드에서 one 변수의 타입이 unknown이지만 타입 단언을 사용해 string 타입으로 단언해주면 one 변수의 문자열 길이를 출력할 수 있습니다.

2) <> 단언

<Type>Value 형식을 사용해 타입 단언을 할 수 있습니다.
let one: unknown = "hello"; let oneLength: number = (<string>one).length; console.log(oneLength); // 5
먼저 살펴봤던 예제 코드를 <>연산자를 사용해 <string>one 형태로 타입을 지정할 수 있습니다. 하지만 React의 .tsx 파일에선 구문 분석에 어려움을 초래할 수 있으므로, 타입 단언을 위해 <>대신 as 연산자를 사용하는 것을 권장합니다.
 
💡
타입 캐스팅(Type Casting)과 타입 단언은 어떤 점이 다를까요? 다른 언어에서 사용되는 비슷한 개념인 타입 캐스팅(Type Casting)와 비교해 보면 사용 방식과 특정 값이 특정 타입임을 컴파일러에 알려주는 점에서 비슷하지만 작동 방식에서 차이가 있습니다.
타입 단언은 런타임에는 아무런 영향을 주지 않기 때문에 실행 에러는 미리 방지하지 못하고, 오직 컴파일 타임에서만  타입을 변화시켜 타입 에러만 해결할 수 있고, 실제 데이터의 타입을 변환시키지 않습니다.
타입 캐스팅은 런타임과 컴파일 타임에서 실제 데이터의 타입을 변경시킨다는 점에서 타입 단언과 차이를 알아볼 수 있습니다.

8.1.2 타입 단언 규칙

이번 절에서는 타입 단언의 규칙에 대해 알아보겠습니다. 타입 단언을 사용할 때는 값과 타입 간의 관계가 중요한데, 이는 부모-자식 관계로 설명될 수 있습니다. Value as <Type> 형식을 가질 때, Value는 Type의 슈퍼 타입(부모 타입)이거나 서브 타입(자식 타입)이어야 합니다. 따라서 이 두 가지 조건 중 하나는 반드시 만족해야 합니다.
let one = 'hello' as number; // Error let two = 100 as string; // Error let three = true as object; // Error
위의 코드에서는 Value와 Type의 관계가 서로 호환되지 않아 에러가 발생합니다. 이를 호환 가능한 타입으로 고치면 아래와 같습니다.
let one = 'hello' as unknown; // OK let two = 100 as unknown; // OK let three = true as never; // OK
하지만 기존 타입과 호환되지 않는 타입으로 단언할 필요가 있는 상황이 발생합니다. 이 경우 다중 단언을 사용해 나타낼 수 있습니다. 다중 단언은 목차 8.1.5에서 자세히 알아보도록 하겠습니다.

8.1.3 const 단언

as const는 객체, 배열, 원시 타입 등에 대해 readonly리터럴 타입을 동시에 적용할 수 있는 방법입니다. 모든 속성이 readonly가 되고, 배열이 불변(Immutable)이 되기 때문에 속성을 변경하거나 배열에 요소를 추가하거나 제거하는 것이 불가능합니다.
let arr = [100, 'hello'] as const; arr.push(); // Error
위의 코드에서는 배열 arr을 [100, "hello"]로 초기화하고 as const를 사용해 arr배열을 읽기 전용으로 만들었습니다. 이에 따라 배열은 변경할 수 없는 상태가 되며, 따라서 배열의 속성을 변경하려고 하면 에러가 발생합니다.
let one = 100 as const; // one: 100
위의 코드에서는 one 의 타입이 as const로 단언했기 때문에 리터럴 타입이 되는 걸 확인할 수 있습니다.

8.1.4 Non-Null 단언

Non-Null 연산자는 변수가 null이 아님을 보장하는 단언 방법입니다. ! 연산자 를 사용하면 값이 null 또는 undefined 가 아니라는 것을 컴파일러에 알릴 수 있습니다.
function a(param: string | null | undefined) { param.slice(3); // Error }
위의 코드를 보면 매개변수 param 이 null이거나 undefined일 수도 있기 때문에 string의 메서드인 slice()를 사용할 수 없다는 에러가 발생합니다.
function a(param: string | null | undefined) { param!.slice(3); // OK }
다음과 같이 param 이 null이나 undefined가 아닌 것이 확실하다면 값 뒤에 ! 연산자 를 붙여 오류를 해결할 수 있습니다.

8.1.5 다중 단언

다중 단언은 먼저 unknown이나 any 타입으로 단언한 후, 다시 원하는 타입으로 단언하는 방법입니다. 8.1.2절에서 살펴본 예제에 다중 단언을 적용해 아래와 같이 강제 형 변환을 할 수 있습니다.
let one = 'hello' as unknown as number; console.log(one.toFixed(2)); // Error
위 코드를 살펴보면 먼저 hello를 unknown으로 단언하고, number 타입으로 단언합니다. 이렇게 하면 타입스크립트는 one 변수를 number로 인식해 toFixed() 메서드를 사용해도 에러가 나타나지 않습니다. 하지만 실제로 hello가 number 타입으로 변하는 게 아니기 때문에 런타임에 toFixed() 메서드를 호출하려고 하면 에러가 발생합니다. 따라서 필요한 상황에 맞게 사용하는 것을 권장합니다.

8.1.6 타입 단언의 사용 목적과 주의할 점

이번 절에선 타입 단언은 언제 사용하면 좋은지 알아보고 사용 시 주의해야 할 점에 대해 알아보도록 하겠습니다.

1) try catch 문에서 error가 unknown으로 추론 될 때

try { //.. } catch (error) { console.log(error.message); // Error }
위의 코드를 보면 TypeScript에서는 try-catch 문의 catch 블록에서 잡을 수 있는 오류가 어떤 타입이 될지 알 수 없기 때문에 오류 객체 error의 타입을 unknown으로 추론해 에러가 발생합니다.
try { //.. } catch (error) { const err = error as Error; console.log(err.message); }
위의 코드처럼 errorError 타입으로 단언하면 error의 타입이 unknown이 아닌 Error로 인식되어 관련 기능을 수행할 수 있습니다.

2) DOM 요소에 접근할 때

const img = document.querySelector("img"); // img: HTMLImageElement | null const myImg = document.getElementById("#myImg"); // myImg: HTMLElement | null const nextImg = document.getElementById("#Img"); // nextImg: HTMLElement | null img.src; // Error myImg.src; // Error nextImg.src; // Error
위의 코드를 보면 DOM 요소에 접근할 때 해당 요소가 실제로 존재하는지 확인할 수 없기 때문에 해당 요소의 속성에 접근하려고 하면 오류가 발생합니다. 이런 경우 아래 코드와 같이 타입 단언을 사용해 더 구체적으로 요소의 타입을 지정해 줄 수 있습니다.
const img = document.querySelector("img")!; const myImg = document.getElementById("#myImg") as HTMLImageElement; const nextImg = <HTMLImageElement>document.getElementById("#nextImg"); img.src; // OK myImg.src; // OK nextImg.src; // OK
첫 번째 img 변수의 경우 ! 연산자 를 사용하여 null이 될 가능성이 없다고 타입스크립트에게 알려줌으로써 null이 아니라는 것을 단언하고 있습니다.
두 번째 myImg변수의 경우, as 를 사용하여 더 구체적 타입인 HTMLImageElement 타입으로 단언함으로써 속성에 접근할 수 있게 되었습니다.
세 번째 nextImg변수는 <> 연산자 를 사용해 타입을 단언했습니다.
하지만 이렇게 단언한 후에도 실제로 요소가 존재하는지 확인하지 않고 속성에 접근하면 여전히 런타임 에러가 발생할 수 있습니다.

3) 객체를 먼저 선언하고 나중에 속성을 추가하려 할 때

타입 단언은 객체를 먼저 선언하고 나중에 속성을 추가하려 할 때 사용할 수 있습니다. 이 경우 as 연산자를 사용해 변수를 먼저 선언하고, 나중에 필요한 속성을 추가할 수 있습니다.
주의할 점은 실수로 필요한 속성을 누락하거나 잘못된 타입의 값을 할당하는 등의 오류를 발생시킬 수 있으므로 안정성 있는 타입 검사를 위해서 타입 단언보다 타입 선언을 권장하고 있습니다.
타입 선언을 사용하면 할당되는 값이 선언된 타입을 만족하는지 먼저 검사해 오류를 발생시키지만, 타입 단언은 타입의 일부 속성만 사용하거나 속성을 추가한다 해도 오류를 발생시키지 않습니다. 다음 예시를 살펴보며 타입 선언과 타입 단언의 차이를 알아보겠습니다.
type Animal = { name: string; age: number; species: string; }; // 타입 선언 const scar: Animal = {}; // Error // 타입 단언 const simba = {} as Animal; simba.name = "심바"; simba.age = 3; simba.species = "사자";
위의 코드에서 타입 선언한 변수 scar 은 객체에 속성을 정의하지 않았기 때문에 오류가 발생합니다. 반면 타입 단언을 사용한 simba 는 빈 객체를 Animal 타입으로 단언했기 때문에 오류를 발생시키지 않습니다.
// 타입 선언 const scar: Animal = { name: "스카", // Error }; // 타입 단언 const simba: Animal = { name: "심바", } as Animal;
위의 코드에서 타입 선언과 달리 타입 단언을 사용한 simba는 객체의 일부 속성만 사용하거나, 속성을 추가해도 에러를 발생시키지 않습니다.

4) 함수의 반환 값으로 union 타입을 사용할 때

const addOrConcat = ( a:number, b:number, c:"add"|"concat"): number|string => { if (c === "add") { return a + b; } else { return "" + a + b; } }; // string으로 단언한 경우 let myVal: string = addOrConcat(2, 2, "concat") as string; console.log(myVal); // 출력 : '22' // number로 단언한 경우 - // 컴파일 오류는 없지만 number가 아닌 string을 반환 let wrongVal: number = addOrConcat(3, 3, "concat") as number; console.log( typeof wrongVal ); // string console.log( wrongVal ); // '33' console.log( wrongVal.toFixed(2)); // Error
위의 코드에서 addOrConcat 함수는 매개변수 c의 값에 따라 다른 타입의 결과를 반환합니다. 이 함수의 반환 값은 number 또는 string인 union 타입이기 때문에, myVal 변수에 대해 타입 단언을 하지 않으면 타입스크립트에서는 에러가 발생합니다.
이 문제를 해결하기 위해서는 myVal 변수에 대해 string으로 타입 단언을 통해 반환 값의 타입을 명확하게 지정해야 합니다. 그러나 wrongVal 변수의 경우에는 다른 문제가 발생합니다. wrongVal 변수는 실제로는 문자열을 반환하지만, number로 타입 단언을 하게 되면 컴파일러는 이를 number 타입으로 인식합니다. 그러나, 실제 실행 시점에서는 string 타입으로 인식됩니다. wrongVal의 타입을 출력하면 string이 출력되는 것을 확인할 수 있습니다. toFixed(2)메서드를 wrongVal에 적용하려고 하면, 실제 wrongVal의 값은 문자열이므로 toFixed()메서드가 존재하지 않아 실행 시점에서 에러를 발생합니다.
따라서 타입 단언은 컴파일러에 특정 값의 타입을 알려주는 역할만 하며, 실제 값의 존재 여부나 타입을 보장하지는 않습니다. 그래서 변수의 실제 값이 존재하는지, 예상한 타입이 맞는지 확인하려면 실행 시점에서 타입 가드를 사용해야 합니다.
 

8.2 타입 가드 (Type Guard)

8.2.1 타입 가드란?

타입 가드는 타입의 안정성을 보장하기 위해 변수나 객체의 타입을 좁혀나가는 것을 의미합니다. 타입 가드가 필요한 이유를 이전 목차에서 문제가 있었던 코드와 함께 알아보겠습니다.
const addOrConcat = ( a:number, b:number, c:"add"|"concat"): number|string => { if (c === "add") { return a + b; } else { return "" + a + b; } }; // string으로 단언한 경우 let myVal: string = addOrConcat(2, 2, "concat") as string; console.log(myVal); // '22' // number로 단언한 경우 - // 컴파일 오류는 없지만 number가 아닌 string을 반환 let wrongVal: number = addOrConcat(3, 3, "concat") as number; console.log( typeof wrongVal ); // string console.log( wrongVal ); // '33' console.log( wrongVal.toFixed(2)); // Error
이전 목차에서 살펴봤던 위의 코드는 타입 단언을 잘못 사용하여 실제 값의 타입과 단언한 타입이 일치하지 않아 런타임에서 에러가 발생한다는 문제를 가지고 있습니다. 이 문제를 해결하기 위해 아래와 같이 타입 가드를 적용할 수 있습니다.
const addOrConcat = ( a:number, b:number, c:"add"|"concat"): number|string => { if (c === "add") { return a + b; } else { return "" + a + b; } }; let myVal = addOrConcat(2, 2, "concat"); if (typeof myVal === "string") { console.log(myVal); // '22' } let wrongVal = addOrConcat(3, 3, "concat"); if (typeof wrongVal === "number") { console.log(wrongVal.toFixed(2)); } else { console.log("wrongVal은 숫자가 아니라 문자열입니다."); console.log(wrongVal); // '33' }
typeof를 사용하여 myValwrongVal의 실제 타입을 체크하고, 이에 따라 적절한 로직을 실행하도록 수정했습니다. 이렇게 타입을 좁혀 실제 타입에 따라 다른 로직을 실행하도록 타입 가드를 적용할 수 있습니다.
타입 가드를 통해 더 정확한 타입을 명시할 수 있게 되므로 코드의 명확성이 향상되고, 버그 발생 가능성이 감소하며, 타입스크립트의 자동 완성 기능도 더욱 효과적으로 사용할 수 있게 됩니다. 타입 가드를 적용하는 방법으로는 typeof, Array.isArray(), instanceof, in, 리터럴, null, undefined, 사용자 정의 등이 있으며 다음 목차에서 적용 방법을 하나씩 알아보도록 하겠습니다.

8.2.2 타입 가드 적용 방법

1) typeof

변수의 타입을 문자열로 반환하는 typeof를 이용해 타입 가드를 구현할 수 있습니다.
function doSomething(x: string | number) { if (typeof x === 'string') { // 타입스크립트는 조건문 블록 안에 있는 x의 타입을 문자열로 간주 console.log(x.toUpperCase()); // 정상 작동! console.log(x.toFixed(1)); // Error } x.toUpperCase(); // Error x.toFixed(1); // Error }
위의 코드에서 if 문에 typeof를 사용해 x의 타입이 문자열일 경우에만 해당 명령문이 실행되도록 타입 가드를 적용했습니다. if 문 블록 안에서의 x는 문자열로 간주하기 때문에 문자열에 사용되는 toUpperCase()메서드는 정상적으로 사용이 가능하지만, 숫자형에 사용하는 메서드인 toFixed()는 사용할 수 없고 오류가 발생하게 됩니다. 또, if 문을 벗어나게 되면 x의 타입이 명확하지 않아 문자열 또는 숫자형 메서드를 사용했을 때 오류가 발생하게 됩니다.

2) Array.isArray()

전달된 값이 배열인지를 판단하는 Array.isArray()를 통해 타입 가드를 구현할 수 있습니다.
function findArray(x: number[] | number): void { if (Array.isArray(x)) { // x가 number[]일 때 실행됩니다. console.log("x는 숫자형 배열입니다."); for (const item of x) { console.log(item); } } else { // x가 number일 때 실행됩니다. console.log("x는 숫자형 입니다."); console.log(x); } }
findArray 함수는 숫자형 배열 또는 단일 숫자인 매개변수 x를 사용합니다. Array.isArray() 를 사용해 x가 배열인지 확인합니다. findArray 함수에서 x가 배열일 경우 if 문의 조건인 Array.isArray(x)true로 if 문 안의 명령이 실행되게 됩니다. 이처럼 x가 배열일 경우에만 명령을 실행하도록 타입 가드를 만들 수 있습니다.

3) in

객체가 특정 프로퍼티를 가지고 있는지 확인하는데 사용되는 in을 이용해 타입 가드를 구현할 수 있습니다.
interface Shoes { size: number; } interface Muffler { length: number; } function itemSize(item: Shoes | Muffler): string { if ('length' in item) { // length 프로퍼티를 가진 경우에만 실행 return `머플러의 길이는 ${item.length}cm 입니다!`; } else { return `신발의 사이즈는 ${item.size}mm 입니다!`; } } const shoes: Shoes = { size: 230 }; const muffler: Muffler = { length: 180 }; console.log(itemSize(shoes)); // 신발의 사이즈는 230mm 입니다! console.log(itemSize(muffler)); // 머플러의 길이는 180cm 입니다!
위의 코드에는 ShoesMuffler라는 두 가지 인터페이스가 있습니다. Shoes에는 number 유형의 단일 프로퍼티 size가 있고, Muffler에는 number 유형의 단일 프로퍼티 length가 있습니다. itemSize 함수는 Shoes 또는 Muffler 객체일 수 있습니다. 함수 내부에서 in 연산자를 사용하여 length 속성이 item에 있는지 확인하는 타입 가드를 적용합니다. length가 있으면 item이 Muffler, length가 없으면 item이 Shoes임을 의미하며 이에 맞는 명령문이 실행됩니다.

4) instanceof

객체가 특정 클래스의 인스턴스인지 확인하는 용도로 사용되는 instanceof를 통해 타입 가드를 구현할 수 있습니다.
class Book { read(): string { return "책을 읽습니다!"; } } class Food { eat(): string { return "음식을 먹습니다!"; } } function action(x: Book | Food): string { if (x instanceof Book) { return x.read(); } else { return x.eat(); } } const myBook = new Book(); const myFood = new Food(); console.log(action(myBook)); // 책을 읽습니다! console.log(action(myFood)); // 음식을 먹습니다!
위의 코드에는 BookFood라는 두 개의 클래스가 있으며 각각 고유한 메서드를 가지고 있습니다. action 함수는 Book 또는 Food 타입을 가지며 함수 내부에서 instanceof를 사용하여 'x'가 Book 클래스의 인스턴스인지 확인합니다. Book 클래스의 인스턴스가 맞다면 read 메서드를 호출하고, 아니라면 Food라고 가정하고 eat 메서드를 호출합니다. 이렇게 instanceof를 사용하여 함수 내의 유형을 좁히는 타입 가드를 만들 수 있습니다.

5) 리터럴

특정 리터럴 값을 기반으로하는 타입 가드를 구현할 수 있습니다.
type Feeling = 'happy' | 'angry' | 'sad'; function modeStatus(feeling: Feeling) { switch (feeling) { case 'happy': console.log("나는 행복해🥰"); break; case 'angry': console.log("나는 화났어😡"); break; case 'sad': console.log("나는 슬퍼😢"); break; default: console.log("happy, angry, sad 중 한 가지를 입력해 주세요!"); } }
위의 코드에서 Feeling 타입은 문자열 리터럴 결합('happy' | 'angry' | 'sad')을 사용하여 생성되므로, Feeling 타입으로 지정된 변수는 3가지 문자열 값 중 하나만 취할 수 있습니다. modeStatus 함수는 Feeling 타입으로 지정된 feeling 매개변수의 값을 평가하기 위해 switch 문을 사용합니다. feeling의 값이 케이스와 일치하면 특정 메시지를 콘솔창에 출력하도록 리터럴 값을 사용한 타입 가드를 적용했습니다.

6) null과 undefined

nullundefined를 사용해 타입 가드를 구현할 수 있습니다.
type A = number | null | undefined; function whatIsX(x: A): string { if (x === null) { // null을 사용한 타입 가드 return 'x는 null 입니다.'; } else if (x === undefined) { // undefined를 사용한 타입 가드 return 'x는 undefined 입니다.'; } else { return `x는 ${x} 입니다.`; } } const numberX: A = 10; const nullX: A = null; const undefinedX: A = undefined; console.log(whatIsX(numberX)); // x는 10 입니다. console.log(whatIsX(nullX)); // x는 null 입니다. console.log(whatIsX(undefinedX)); // x는 undefined 입니다.
위 코드에서 A라는 타입은 숫자, null, undefined로 지정됩니다. A 타입으로 지정된 변수는 세 가지 값 중 하나를 가집니다. whatIsX 함수는 A 타입의 매개변수 x를 받습니다. 함수 내에서 x의 범위를 좁히기 위해 null과 undefined를 사용한 동등성 검사 조건문으로 타입 가드를 만들 수 있습니다.

7) 사용자 정의 타입 가드

타입스크립트에서는 사용자 정의 타입 가드 함수를 만들어 사용할 수 있습니다. 사용자 정의 타입 가드 함수는 리턴값에 is 연산자를 사용해 타입을 명시합니다. 이름을 입력하지 않는 사람을 찾아내는 사용자 정의 타입 가드 함수를 만들어보며 적용 방법을 알아보도록 하겠습니다.
우선 사용자의 정보를 담는 User 인터페이스를 정의하겠습니다.
interface User { name: string | null; age: number; }
User 인터페이스는 사용자의 이름(name)과 나이(age)를 프로퍼티로 가지며, name은 string 타입 또는 null을 가질 수 있습니다. 즉, 이름을 입력하지 않은 사용자는 name 프로퍼티가 null인 User 객체입니다.
그럼 이제 이름을 입력하지 않은 사용자를 찾아내는 사용자 정의 타입 가드 함수를 만들어 보겠습니다.
function hasNoName(user: User): user is User & { name: null } { // is 연산자를 사용해 타입을 명시합니다 return user.name === null; } const user: User = getUser(); // getUser는 User 타입의 객체를 반환하는 함수라고 가정합니다. if (hasNoName(user)) { console.log('이름이 입력되지 않은 사용자입니다.'); } else { console.log(`사용자의 이름은 ${user.name}입니다.`); }
위 코드에서 hasNoName 함수는 User 타입의 객체를 받아서, 그 객체의 name 프로퍼티가 null인지를 검사하는 사용자 정의 타입 가드 함수입니다. 이 함수는 user is User & { name: null } 형태의 반환 타입을 가지며, 이는 user의 name 프로퍼티가 null이면 true를 반환한다는 것을 의미합니다. 이렇게 타입 가드를 통해 타입스크립트는 if (hasNoName(user)) 문이 true일 때 user의 name 프로퍼티가 null임을, false일 때 user의 name 프로퍼티가 string임을 알아낼 수 있습니다.

8) 서로소 유니온 타입

서로소 유니온 타입은 타입 가드를 할 때 직관적으로 객체 타입을 정의하는 방법입니다. 교집합이 없는 타입으로만 만든 유니온 타입이라고 할 수 있습니다.
type Admin = { name: string; kickCount: number; }; type Member = { name: string; point: number; }; type Guest = { name: string; visitCount: number; }; // union type type User = Admin | Member | Guest;
세 객체를 합쳐 User라는 union 타입을 만들었습니다.
function func(user: User) { // admin if ("kickCount" in user) { console.log(`${user.name}님 지금까지 ${user.kickCount}명 내보내셨습니다.`); // member } else if ("point" in user) { console.log(`${user.name}님 지금까지 ${user.point}포인트를 모으셨습니다.`); // guest } else { console.log(`${user.name}님 지금까지 ${user.visitCount}번 방문하셨습니다.`); } }
하지만 이대로 User를 사용할 경우, 주석이 없다면 각 타입 가드가 어떠한 타입을 말하는지 확인하기 어렵습니다. 의도와는 다르게 추가로 각 타입의 프로퍼티를 확인하며 사용해야 합니다.
이때 tag 프로퍼티를 추가하여 타입 좁히기를 직관적으로 변경할 수 있습니다. tag 프로퍼티는 리터럴 타입으로 정의 되어 있기 때문에 1가지의 값만 가지게 되므로 각 타입은 독립적으로 교집합 없이 존재하게 됩니다.
// tag가 ADMIN이면서 동시에 MEMBER인 경우는 존재할 수 없다. // 즉, 교집합이 없이 서로소 관계이다. type Admin = { tag: "ADMIN"; name: string; kickCount: number; }; type Member = { tag: "MEMBER"; name: string; point: number; }; type Guest = { tag: "GUEST"; name: string; visitCount: number; }; type User = Admin | Member | Guest; function func(user: User) { // admin if (user.tag === "ADMIN") { console.log(`${user.name}님 지금까지 ${user.kickCount}명 내보내셨습니다.`); // member } else if (user.tag === "MEMBER") { console.log(`${user.name}님 지금까지 ${user.point}포인트를 모으셨습니다.`); // guest } else { console.log(`${user.name}님 지금까지 ${user.visitCount}번 방문하셨습니다.`); } }
객체에 선택적 프로퍼티가 정의되어 있는 경우 옵셔널 체이닝이나 조건문을 추가하여 해결하는 것보다 타입을 분리하여 서로소 유니온 타입으로 만들어 해결하는 게 직관적이고 안전합니다.