타입스크립트에 존재하는 두 가지 핵심 추론 개념인 union과 narrowing에 대해 알아보자.
union(유니언)
union은 허용된 타입 값을 두 개 이상의 타입으로 확장하는 것이다. 정확히 어떤 값인지는 모르지만, 여러 타입 중 하나인 것을 알고 있을 때 유용하다.
타입 선언
union은 |
연산자를 이용하여 타입을 구분하여 선언한다. 타입 선언의 순서는 중요하지 않다.
let sec: string | number;
변수의 초깃값이 있음에도 명시적으로 타입 값을 할당해야 할 때 주로 사용한다.
let sec: string | null = null;
if (new Date().getSeconds() > 30) {
sec = 'Time out';
}
유니언 속성
유니언 타입을 가진 값은 선언한 모든 타입에 존재하는 속성에만 접근할 수 있다. 예를 들어, string | number 타입을 가진 경우 toString()
속성은 사용할 수 있으나 toUpperCase()
나 toExponential()
속성은 사용할 수 없다.
let sec = new Date().getSeconds() > 30 ? 'Time out' : 10;
sec.toString();
sec.toUpperCase(); // Error
sec.toExponential(); // Error
타입스크립트는 타입이 확실하게 정해져 있지 않은 경우, 어느 하나라도 속하지 않은 속성을 사용하려고 하는 것은 안전하지 않다고 여긴다.
narrowing(내로잉)
narrowing은 이전에 선언된 타입보다 더 구체적으로 유추하여 타입을 좁히는 것이다. 타입을 좁히는 데 사용하는 논리적 검사를 type guard(타입 가드)라고 한다.
typeof 검사
자주 사용하는 타입 가드는 typeof
연산자를 사용하는 것이다. typeof로 해당 값의 타입이 조건과 일치하면 조건문 내의 코드를 실행한다.
let sec = new Date().getSeconds() > 30 ? 'Time out' : 10;
if (typeof sec === 'string') {
sec.toUpperCase(); // TIME OUT
}
else 문이나 삼항 연산자를 사용해 작성할 수도 있다.
let sec = new Date().getSeconds() > 30 ? 'Time out' : 10;
if (!(typeof sec === 'string')) {
sec.toExponential(); // 1e+1
} else {
sec.toUpperCase(); // TIME OUT
}
typeof sec === 'string' ? sec.toUpperCase() : sec.toExponential();
값 할당
변수에 값을 할당하는 것으로도 타입을 좁힐 수 있다. 아래 코드에서 sec
변수는 초기에 string | number 타입을 선언했지만, 값이 할당된 후 string 타입으로 변한 것을 확인할 수 있다.
let sec: string | number;
sec = 'Time out'; // sec: string
sec.toUpperCase(); // TIME OUT
Truthiness
Truthiness narrowing은 truthy 값이 if 문이나 && 연산자에서 true로 간주되는 성질을 이용한 것이다. 변수가 string | undefined 타입인 경우, undefined는 항상 falsy 값이므로 if 문 내에서 변수는 string 값이라고 추론한다.
let sec = new Date().getSeconds() > 30 ? 'Time out' : undefined;
// if 문 내에서만 string
if (sec) {
sec.toUpperCase(); // TIME OUT
}
sec.toUpperCase(); // Error
&& 연산자일 때도 같은 논리이다.
let sec = new Date().getSeconds() > 30 && 'Time out';
if (sec) {
sec; // string
} else {
sec; // false | string
}
literal(리터럴) 타입
리터럴 타입은 어떠한 원시 값 중 하나가 아닌, 특정 원시 값을 가진 타입이다. const로 선언한 변수에 값을 직접 할당하면 타입스크립트가 해당 변수를 할당된 리터럴 값으로 유추한다.
const physicist = 'Richard Feynman'; // const physicist: "Richard Feynman";
let physicist = 'Richard Feynman'; // let physicist: string;
유니언 타입에서는 리터럴 타입과 원시 타입을 섞어 사용할 수 있다.
let physicist: boolean | 'Richard Feynman' | 'Schrödinger';
physicist = false;
physicist = 'Schrödinger';
physicist = 100; // Error
리터럴 타입으로 선언한 변수에 원시 타입 값을 할당할 수는 없지만, 원시 타입 변수에 리터럴 타입은 할당할 수 있다.
let physicist: 'Schrödinger';
let foo = 'a';
physicist = 'Maxwell'; // Error
foo = 'Maxwell';
타입 별칭
만약 타입을 여러 변수에 재사용하고 싶다면 타입 별칭을 사용하면 된다. 타입 별칭은 type 원하는 별칭 = 타입
형태로 지정한다.
type SomeType = string | number | boolean | null;
타입스크립트는 타입 별칭을 발견하면 해당 별칭이 참조하는 타입을 입력한 것처럼 작동한다. 다만, 타입스크립트의 타입 시스템에만 존재하기 때문에 런타임 코드에서는 참조할 수 없다.
type SomeType = string | number | boolean | null;
let foo: SomeType;
let bar: SomeType;
let baz: SomeType;
console.log(SomeType); // Error
또한, 타입 별칭끼리 결합하여 사용할 수도 있다.
type SomeType = string | number;
type OtherType = SomeType | boolean | null;