Frontend/Typescript

러닝 타입스크립트 - 3장. 유니언과 리터럴

rachel_13 2023. 7. 4. 09:46

 

  • 유니언(Union): 값에 허용된 타입을 두 개 이상의 가능한 타입으로 확장하는 것
  • 내로잉(Narrowing): 값에 허용된 타입이 하나 이상의 가능한 타입이 되지 않도록 좁혀가는 것

1. 유니언 타입으로 두 개 이상의 타입 중 하나일 수 있는 값을 나타내는 방법

let math = Math.random() > 0.5 ? undefined : "math"
//math type: string | undefined 이거 혹은 저거인 타입을 유니언 타입이라고 함

` | (수직선 연산자)`를 이용해서 표시한다.

 

 

2. 타입 어노테이션으로 유니언 타입을 명시적으로 표시하는 방법

변수에 대한 명시적 타입 어노테이션을 제공하는 것이 유용할 때 사용하며,

사용자가 잠재적으로 다른 타입이 될 수 있음을 알고 있을 때 타입의 값을 할당할 수 있다.

 

아래 예제에서 thinker는 초기값이 null 이지만, 잠재적으로 string 속성이 될 수 있음을 알려준다.

명시적으로 타입 어노테이션을 활용하여 유니언 타입을 선언하면, 타입스크립트는 string으로 변수에 값을 할당했을 때 오류를 발생시키지 않는다.

let thinker: string | null = null;

if(Math.random() > 0.5){
    thinker = "Math Lover"; //Ok
}

 

[속성]

1) 유니언 타입 선언 순서는 중요하지 않다.

    boolean | string 이나 string | boolean이나 같음

 

2) 유니언 타입으로 타입을 선언한 경우, 유니언 타입외의 속성에 접근하게 되면 타입 오류를 발생시킨다.

 

유니언 타입에 속하지 않는 속성에 대한 접근을 제한하는 것은 일종의 안전 장치이다.

let developer = Math.random() > 0.5 ? "Michel": 84;

developer.toString();

developer.toUpperCase();
/**
 * Property 'toUpperCase' does not exist on type 'string | number'.
 * Property 'toUpperCase' does not exist on type 'number'. 
 */
 
 developer.toFixed();
 
 /**
  * Property 'toFixed' does not exist on type 'string | number'.
  * Property 'toFixed' does not exist on type 'string'.
  */

객체가 어떤 속성을 포함하고 있는지 모르는 경우, 타입스크립트는 해당 속성을 사용하는 것이 안전하지 않다고 생각하고 오류를 발생시킨다.  왜냐하면, 그 속성이 진짜로 있는지 없는지 알 수 없기 때문이다.

 

위 예제에서도 타입이 string | number 이기 때문에 두 가지 타입에 모두 존재하는 toString()은 사용할 수 있지만,

toUpperCase()처럼 string에만 사용 가능한 속성이고,  toFixed()처럼 number에만 사용 가능한 속성은 사용할 수 없다고 오류를 발생시킨다.

 

이럴 때는 타입을 특정 타입으로 한정지어서 사용할 수 있는데, 이러한 기법을 "내로잉(narrowing)" 이라고 한다.

 

3. 타입 내로잉으로 값의 가능한 타입을 좁히는 방법

내로잉(narrowing) 정의 : 구체적인 타입을 명시함으로써 이전에 알려진 것보다 타입이 더 좁혀졌다는 것을 알리는 것

타입을 좁히는 두 가지 방법을 알아보자. (이 때 사용되는 논리적 검사를 타입 가드(type guard)라고 함)

 

1) 값 할당해서 타입 좁히기

let inventor: number | string = "Heny Lamarr"; // 값 할당으로 문자열로 타입이 바로 좁혀짐

inventor.toUpperCase(); //Ok
inventor.toFixed(); //Error: Property 'toFixed' does not on type 'string',

타입 어노테이션으로 number 또는 string 타입을 사용할 수 있도록 변수를 선언하였지만,

선언과 동시에 문자열을 할당함으로써, 타입이 string 으로 좁혀졌다.

따라서, 문자열에 사용할 수 있는 속성만 사용가능하고, 그 외의 속성에는 접근할 수 없도록 오류를 발생시킨다.

 

2) 조건 검사를 통한 타입 좁히기

if문을 통해 변수에 할당된 값과 선언한 타입이 일치할 경우, 타입에 알맞는 속성을 사용할 수 있도록 제한함으로써 타입 좁히기를 할 수 있다.

타입스크립트는 if문 내에서 변수가 알려진 값과 동일한 타입인지 확인한다.

let developer = Math.random() > 0.5 ? "Rachel" : 28; //type: string | number

if(developer === "Rachel"){
	//developer의 type이 string으로 좁혀짐
    developer.toUpperCase();
}

if(developer === 28){
	//developer의 type이 number로 좁혀짐
    developer.toFixed();
}

developer.toUpperCase(); //type이 string인지 number인지 확정할 수 없음
/**
 * Error : Property 'toUpperCase' does not exist on type '"Rachel" | 28'.
 * Property 'toUpperCase' does not exist on type '28'.
 */

변수가 여러 타입 중 하나일 때, 조건부 로직으로 타입을 좁히게 되면 필요한 타입과 관련된 검사만 하게되므로 강제로 코드를 안전하게 작성할 수 있다.

 

 

3) typeof 검사를 통한 타입 좁히기

2)번 처럼 직접 값을 확인할 수도 있지만, typeof 연산자를 사용할 수도 있다.

 

위 예제는 다음과 같이 typeof 연산자를 사용해서 타입을 좁힐 수도 있다.

let developer = Math.random() > 0.5 ? "Rachel" : 28;

if(typeof developer === "string"){
    developer.toUpperCase();
}

if(typeof developer === "number"){
    developer.toFixed();
}

developer.toUpperCase();
/**
 * Property 'toUpperCase' does not exist on type '"Rachel" | 28'.
  Property 'toUpperCase' does not exist on type '28'.
 */

 

! 을 사용한 논리 부정이나, else 문, 삼항연산자 모두 사용가능하니 참고하자.

typeof developer === "string"
	? developer.toUpperCase() //Ok: string
    : developer.toFixed() //Ok: number

 

4) 참 검사를 통한 타입 좁히기

자바스크립트에서는 또는 truthy는 if문 / && 연산자 등의 Boolean 문맥에서 true로 간주되며,

false, 0, -0, 0n, "", null, undefined, NaN 같은 falsy로 정의된 값을 제외하고는 모두 참이다.

 

타입스크립트는 truthy로 확인된 일부에 대해서만 변수의 타입을 좁힐 수 있다.

let geneticist = Math.random() > 0.5 ? "Barbara McClintock" : undefined; //type: string | undefined

if(geneticist){
  geneticist.toUpperCase(); //Ok: string
}

  geneticist.toUpperCase(); //Error: 'geneticist' is possibly 'undefined'.(18048)

undefined는 항상 falsy 이므로 if 문 코드 블록에서는 geneticist가 string 타입으로 좁혀진다.

 

논리연산자인 &&, ? 는 참 여부를 검사하는 일도 잘 수행하지만, 알고 있는 값이 falsy라면, 그 타입에 대해 좁힐 수 없다.

const a = geneticist && geneticist.toUpperCase(); //a type: string | undefined

const b = geneticist?.toUpperCase(); //b type: string | undefined

 

아래 예제에서 biologist는 false | string 타입인데 if문 블록안에서는 string으로 타입이 좁혀진다.

els문에서는 다시 원래 타입으로 돌아가서 유니언 타입이 나온다.

let biologist = Math.random() > 0.5 && "Rachel Carson"; //type: string | fasle;

if(biologist){
  biologist; //type: string
}else {
  biologist; //type: string | false
}

 

4. 리터럴 타입의 const 변수와 원시 타입의 let 변수의 차이점

리터럴 타입(literal type) : 원시 타입 값 중 어떤 것이 아닌 특별한 원시값 (only 개념)

 

변수를 const로 선언하고 직접 리터럴 값을 할당하면 타입스크립트는 해당 변수를 할당된 리터럴 값으로 유추한다.

(let 으로 선언하면 유일한 변수가 아니라 값이 재할당 될 수 있으므로 범용적인 원시값으로 선언되지만, const 의 경우 유일한 변수/상수 값으로 선언하므로 리터럴 타입으로 선언된다.)

 

즉, 원시 타입은 해당 타입이 가질 수 있는 모든 리터럴 타입의 집합체이다.

모든 원시타입에는 무수히 많은 리터럴 타입이 존재할 수 있다.

추가적으로 유니언 타입에는 원시 타입과 리터럴 타입을 혼용해서 사용할 수 있따.

 

let lifespan: number | "ongoing" | "uncertain";

lifespan = 90; //Ok: number
lifespan = "ongoing" //Ok: "ongoing"
lifespan = "uncertain" //Ok: "uncertain"

lifespan = true; //Error: Type 'true' is not assignable to type 'number | "ongoing" | 'uncertain'

 

5. '십억 달러의 실수'와 타입스크립트가 엄격한 null 검사를 처리하는 방법

"엄격한 null 검사" : 잠재적으로 정의되지 않은 undefined 값을 체크하는 작업

타입을 좁히는 것이 이 "엄격한 null 검사"시 유용하다.

 

타입스크립트 컴파일러는 옵션 중 strictNullChecks는 엄격한 null 검사를 활성화할지 여부를 결정한다.

  • strictNullChecks: false 일 경우

코드의 모든 타입에 | null | undefined를 추가해야 할당할 수 있다.

strictNullChecks 옵션을 false로 설정하면 nameMaybe 변수가 toLowerCase에 접근할 때 undefined가 되는 것은 잘못된 것이다.

let nameMaybe = Math.random() > 0.5 ? "Tony" : undefined;

nameMaybe.toLowerCase(); //Potential Runtime Error: Cannot read property 'toLowerCase' of undefined.

 

  • strictNullChecks: true 일 경우
let nameMaybe = Math.random() > 0.5 ? "Tony" : undefined;

nameMaybe.toLowerCase(); //'nameMaybe' is possibly 'undefined'.

활성화할 경우, 타입스크립트는 잠재적 충돌 오류를 감지한다.

따라서, 웬만하면.. strictNullChecks 옵션은 true로 설정하자. 그래야 null/undefined으로 인한 오류를 파악하기가 쉽다.

 

6. 존재하지 않을 수 있는 값을 나타내는 명시적인 | undefined 와
     할당되지 않은 변수를 위한 암묵적인 | undefined

자바스크립트에서는 일반적으로 초기값이 없으면 undefined가 된다.

만약, 타입스크립트에서 undefined를 포함하지 않는 타입으로 변수를 선언한 다음,

값을 할당하기 전에 사용하려고 하면 어떻게 될까?

 

기본적으로 타입스크립트는 값이 할당되기 전에는 변수의 타입이 undefined라는 사실을 충분히 인지할 만틈 똑똑하다.

값이 할당되기 전에 속성 중 하나에 접근하려고 하면 오류를 발생시킨다.

 

아래 예제를 보자.

let scientist: string;

scientist.length;
//Error: Variable 'scientist' is used before being assigned.(2454)

scientist = "Rachel"
scientist.length; //Ok

 

7. 반복적으로 사용하고 입력이 긴 유니언 타입을 타입 별칭에 저장하는 방법

유니언 타입이 너무 길어진다면, 재사용하기 쉽게 하려고, 타입 별칭을 사용하는 방법이 있다. 

타입을 변수처럼 선언해서 여러곳에서 사용하기 좋게 만드는 방법으로

 

type 이름 = 타입

 

의 형태이다. (* 이때 이름은 파스칼 케이스로 지정함)

아래 예제를 살펴보자.

 

let rawDataFirst: boolean | number | string | null | undefined;
let rawDataSecond: boolean | number | string | null | undefined;
let rawDataThird: boolean | number | string | null | undefined;

동일한 타입이 여러번 사용되었다. 중복된 코드가 발생하고 있다. 줄이고 싶은 충동이 든다.

이렇게 바꿔보면, 훨씬 깔끔해지는 것을 알 수 있다.

 

type RawDataType = boolean | number | string | null | undefined;

let rawDataFirst: RawDataType;
let rawDataSecond: RawDataType;
let rawDataThird: RawDataType;

 

주의할 점은 타입 별칭은 타입스크립트만의 속성으로 자바스크립트로 컴파일될 때 확인되지 않는다.

따라서 런타임에는 참조할 수 없고, 개발시에만 사용할 수 있다는 특징이 있다.

 

 

  • 타입 별칭 결합

타입 별칭은 다른 타입 별칭을 참조할 수 있다.

즉, 다른 타입을 선언시 타입 별칭을 사용하는 것이 가능하다는 점이다.

type DetailRawType = number | null;

type RawDataType = DetailRawType | string | undefined;