...
타입 추론 (Inference)
타입 추론이란, 개발자가 굳이 변수 선언할때 타입을 쓰지않아도 컴파일이 스스로 판단해서 타입을 넣어주는 것을 말한다.
예를들어 다음과 같이 타입을 생략한채 변수를 선언하면 대입되는 값의 자료형태를 보고 컴파일러가 num 변수는 number 타입인걸 추론해서 자동으로 넣게 되는 원리이다.
let num = 12;
num = 'Hello type!'; // Error - TS2322: Type '"Hello type!"' is not assignable to type 'number'.
위와 같이 num에 대한 타입을 따로 지정하지 않더라도 일단 num 변수는 number로 간주되는 걸 확인 할 수 있다.
타입 추론은 함수의 리턴 타입에도 요긴하게 쓰일 수 있다.
다음과 같이 number 타입 두개가 연산을 한 결과값을 리턴하는 함수일 경우, 굳이 함수 타입을 number로 지정 안해줘도 저정도의 추론은 컴파일러가 스스로 판단이 가능하다.
function add(x: number, y: number) { return x + y }; // 리턴 타입 명시 X
const result = add(1, 2); // 리턴 타입을 명시 안해줘도 어차피 number타입끼리의 연산은 number일테니 tsc가 이정도의 추론은 알아서 해준다.
리턴 타입 값이 여러개인 함수도 똑똑하게 추론을 해주며,
function func2(value: number) {
if (value < 10) {
return value;
} else {
return `${value} is too big`;
}
}
심지어 구조분해 코드에서도 자동으로 타입 추론이 되는걸 볼 수 있다.
let arr1 = [10, 20, 30];
let [n1, n2, n3] = arr1; // n1, n2, n3은 number 타입으로 추론 된다.
let obj = { id: "abcd", age: 123, language: "korean" };
let { id, age, language } = obj; // id는 string, age는 number, language는 string으로 추론된다.
이렇게 타입을 생략한채 변수를 선언하거나 초기화 할 때 타입이 추론된다. 이외에도 변수, 속성, 인자의 기본 값, 함수의 반환 값 등을 설정할 때 타입 추론이 일어난다.
타입스크립트가 타입을 추론하는 경우는 다음과 같다고 보면된다.
- 초기화된 변수
- 기본값이 설정된 매개 변수
- 반환 값이 있는 함수
그런데 문득 이런 생각이 들었을 것이다.
보다 타입을 명확히 써서 에러를 줄이고자 탄생한게 타입스크립트인데, 위와 같이 자바스크립트 처럼 써버리면 무슨 의미가 있겠냐는 반론이다. 하지만 프로그래머 세계에선 이는 융통성이 없는 부분이다.
타입스크립트의 강한 타입 규칙은 오히려 생산성을 크게 떨어뜨린다. 하지만 그렇다고 지나친 유연성은 자바스크립트와 같은 꼴이 된다.
바로 이 생산성과 유연성 두마리 토끼를 잡은게 컴파일의 타입 추론 기능이라고 보면 된다.
가장 와닿는 예시는 const 상수를 선언할 상황일 것이다.
myName 이라는 상수를 선언한다고 하자. 다음 두가지 방식 중 어느걸 사용하는게 좋을까?
const myName: string = 'inpa';
const myName = 'inpa';
myName은 상수라 값을 바꾼다거나 재선언하거나 하는 동작은 불가능하다.
그럼 여기에 굳이 :string 이라는 타입을 선언해서 알려줘야 하는 이유가 있을까? 어차피 상수에 문자열 값을 대입했으니 이 상수의 타입은 코드를 뜯어고치지 않는한 문자열 리터럴 타입임에는 불변의 진실이다.
괜히 손가락 마디만 아픈 짓을 하고 있는 것과 같다.
이럴때 그냥 시스템에게 알아서 판단하라고 넘기는 것이다. 그리고 저정도의 판단은 굳이 뛰어난 AI가 아니더라도 가능하다.
그럼 반대로 컴파일러는 어느정도 똑똑할까?
개발자가 완전히 손을 떼도 좋을정도로 아이언맨 뺨치는 자비스 정도면 신경쓰지 않았을 것이다.
그러나 현실은 그렇지 않다. 가끔 컴파일도 타입 추론을 멍청하게 할때도 있다.
예를 들어 다음 const 객체가 있다고 하자.
const obj = {
red: 'apple',
yellow: 'banana',
green: 'cucumber',
};
각 객체 key에 명확히 value가 명시되어 있으니 각 key에 대한 타입은 문자열 리터럴 타입일 것이다.
하지만 실제로 확인해보면 다음과 같이 string으로 되어있는걸 볼수있다. (상수 인데?)
이럴때는 직접 객체 타입을 지정해주거나, 혹은 뒤에서 배울 타입 단언 (as const) 으로 직접 명명해야 한다.
type Constobj = {
red: 'apple';
yellow: 'banana';
green: 'cucumber';
};
const obj: Constobj = {
red: 'apple',
yellow: 'banana',
green: 'cucumber',
};
// ---------------------------------------------------
const obj2 = {
red: 'apple',
yellow: 'banana',
green: 'cucumber',
} as const; // 타입 단언
인터페이스 타입 추론
이번에는 좀 복잡한 타입 추론을 해보겠다.
다음과 같이 Person 인터페이스가 있고 Korean, Japanese는 Person을 확장해서 만든 인터페이스다.
그리고 p1, p2, p3 변수를 생성하고 이들을 배열로 만들어 arr1 과 arr2 변수에 대입해 보았다.
그럼 arr1 과 arr2 의 타입은 어떻게 추론 될까?
이떄는 여러가지 타입을 하나로 통합하는 과정을 거치게 된다.
바로 다른 타입으로 할당 가능한 타입은 제거가 되는데, 예를 들어 Korean과 Japanese 인터페이스는 둘다 Person에 할당이 가능하기 때문에(상속 되었으니까) arr1 변수의 타입은 둘다 제거가 되고 Person 타입만 남게 된다.
그리고 arr2는 Korean과 Japanese 타입의 변수를 받는데, 이 둘은 서로 상속되는 관계가 아니기 때문에 별개의 타입으로 취급된다. 따라서 두개의 타입을 묶는 유니온 타입으로 추론이 되는 것이다.
interface Person {
name: string;
age: number;
}
interface Korean extends Person {
liveInSeoul: boolean;
}
interface Japanese extends Person {
liveInTokyo: boolean;
}
const p1: Person = { name: 'mike', age: 23 };
const p2: Korean = { name: 'mike', age: 25, liveInSeoul: true };
const p3: Japanese = { name: 'mike', age: 27, liveInTokyo: false };
const arr1 = [p1, p2, p3];
// const arr1: Person[]
const arr2 = [p2, p3];
// const arr2: (Korean | Japanese)[]
이부분의 자바(Java)의 객제 지향 클래스 상속 관계에 대해 자세히 알지 못하면 난해한 파트이다.
간단히 정리하자면, 부모 클래스를 상속한 자식 클래스를 new 생성자로 생성할때 변수의 타입을 부모 클래스로 지정해 생성할 수 있다는 원리에서 따온 개념이다.
class 부모클래스 {
String 부모필드;
}
class 자식클래스 extends 부모클래스 {
String 자식필드;
}
부모클래스 변수 = new 자식클래스(); // 부모클래스 타입 변수이지만, 자식클래스는 부모클래스를 상속(포함)하기에, 생성까지는 문제 없음
변수.부모필드; // 사용하는데 문제없음
변수.자식필드; // ERROR - 하지만 할당된 변수는 부모클래스 타입이기 때문에 자식클래스에만 있는 필드를 접근할 경우 에러가 뜨게 된다.
그래서 타입스크립트에서 자식 인터페이스 Korean에만 있는 liveInSeoul 속성에 접근하려고 하면 대입은 문제없는데 사용할때 빨간줄이 뜸을 볼 수 있다.
타입 호환 (Compatibility)
타입 호환이란 단어 그대로 타입스크립트 코드에서 특정 타입 간에 구조가 비슷하면 타입 호환이 될 수 있다는 것을 의미한다.
interface Ironman {
name: string;
}
class Avengers {
name: string;
}
let i: Ironman;
i = new Avengers(); // OK, because of structural typing
위 코드를 보면 인터페이스로 선언한 Ironman 타입을 가진 변수 i가 Avengers 클래스 생성자를 어떤 에러 없이 할당 받은 것을 볼 수 있다. Ironman과 Avengers는 서로 다른 자료형임에도 말이다.
하지만 타입 구성을 보면 name: string 으로 서로 비슷하게 보인다.
사람 눈에도 그렇게 보이듯이 컴파일러도 그렇게 타입 추론 해줌으로서 타입 호환이 되는 것이다.
구조적 타입 호환
다음 Avangers 라는 인터페이스 타입이 있다고 하자. 여기에 name 키 속성 하나만 정의되어 있다.
이 인터페이스를 hero 변수 타입으로 정의하고 객체값을 대입하였다. 그런데 name 키 외에 location 키도 같이 넣었다.
interface Avengers {
name: string;
}
let hero: Avengers;
hero = { name: "Captain", location: "Pangyo" }; // Error !!!!!
당연하게도 타입이 맞질 않으니 이는 에러가 뜨게 되고 값 대입이 불가능하게 된다.
let tmp = { name: "Captain", location: "Pangyo" };
hero = tmp; // 에러없이 할당됨
그런데 이번에는 { name: 'Captain', location: 'Pangyo' } 객체 값을 바로 hero 변수에 대입하지말고 중간에 tmp 변수에 대입한뒤, tmp 변수를 hero 변수에 대입해보면 에러없이 할당된다.
무언가 자바스크립트 냄새가 나는 이 현상은, tmp의 속성 중에 name이 있기 때문에 tmp가 hero 타입에 호환될 수 있는 것이다.
즉, Avengers 인터페이스에서 name 속성을 갖고 있기 때문에 tmp는 Avengers 타입에 호환될 수 있다고 컴파일러가 추론한 것이다.
함수를 호출할 때도 마찬가지이다.
function assemble(a: Avengers) {
console.log("어벤져스 모여라", a.name);
}
// 위에서 정의한 tmp 변수. 타입은 { name: string; location: string; } 임에도 인자 타입은 Avengers와 호환된다.
assemble(tmp);
이러한 현상을 전문적인 용어로 '잉여 속성 검사 원리' 라고 칭하는데, 보다 자세한 원리를 알고싶다면 다음 포스팅을 참고하길 바란다.
Union 타입 호환
다음과 같이 객체 타입이 있다고 하고, 두 객체 타입을 & 연산자 (인터렉션) 으로 묶었다. 그럼 당연히 변수 b와 c 에는 형식이 맞질 않아 에러가 나게 된다.
type A = { num: number } & { str: string };
const a: A = { num: 1, str: '1' }; // Ok - 모든 속성이 다 있어야 한다.
const b: A = { num: 1 }; // Error
const c: A = { str: '1' }; // Error
그럼 & 연산자를, | 연산자 (유니온) 으로 바꿔보자. 놀랍게도 에러가 사라진다.
type A = { num: number } | { str: string };
// 두 객체 속성중에 하나만 있어도 되고 두개 모두 있어도 된다
const a: A = { num: 1, str: '1' };
const b: A = { num: 1 };
const c: A = { str: '1' };
{ num: number } 또는 { str: string } 이니까 당연한 수순이다.
여기서 눈요기 해봐야 할 부분은 바로 두 객체 타입을 동시에 썼을 때이다. const a: A = { num: 1, str: '1' }
and 연산이 아님에도 두 객체를 동시에 선언해도 문제가 없다.
이것이 바로 유니온 타입 호환이다.
단순하게 and 연산과 or 연산을 떠올려 대입해서 생각해보면 이해가 가질 않을 것이다. 어쩔수 없이 이부분은 단순 암기로 넘어가야 한다. 🥴
Interface 타입 호환
똑같은 인터페이스명을 중복으로 선언하면 안의 인스턴스들이 서로 합쳐져 호환된다.
마치 컴파일러가 알아서 자동으로 하나로 합쳐준다고 생각하면 된다.
interface A {
talk: () => void;
}
interface A {
eat: () => void;
}
interface A {
shit: () => void;
}
const a: A = {
talk() {
return;
},
eat() {
return;
},
shit() {
return;
},
};
Enum 타입 호환
enum 타입은 number 타입과 호환되지만 이넘 타입끼리는 호환되지 않는다.
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let statuss = Status.Ready;
statuss = 0;
statuss = Color.Green; // Error
Function 타입 호환
함수 끼리도 타입 호환은 호출하는 시점에 문제가 없어야 할당이 가능하다.
다음은 함수 타입 A가 함수 타입 B로 할당 가능하기 위한 조건은 다음과 같다..
- A의 매개변수 개수가 B의 매개변수 개수보다 적어야 한다.
- 같은 위치의 매개변수에 대해 B의 매개변수가 A의 매개변수로 할당 가능해야 한다.
- A의 반환값은 B의 반환값으로 할당 가능해야 한다.
/**
* 다음은 함수 타입 A가 함수 타입 B로 할당 가능하기 위한 조건이다.
* 1. A의 매개변수 개수가 B의 매개변수 개수보다 적어야 한다.
* 2. 같은 위치의 매개변수에 대해 B의 매개변수가 A의 매개변수로 할당 가능해야 한다.
* 3. A의 반환값은 B의 반환값으로 할당 가능해야 한다.
*/
type F1 = (a: number, b: string) => string;
type F2 = (a: number, b: string | number) => string;
type F3 = (a: number) => string;
type F4 = (a: number) => number | string;
let f1: F1 = (a, b) => `${a} ${b.length}`;
let f2: F2 = (a, b) => `${a} ${b}`;
let f3: F3 = (a) => `${a}`;
let f4: F4 = (a) => (a < 10 ? a : 'too big');
f1 = f3; // f1 보다 f3 매개변수가 더 적기 때문에 가능 (1번 항목)
f3 = f1; // ERROR !!
f1 = f2; // f2의 매개변수 b가 유니온 타입이기에, f1 에도 할당이 가능 (2번 항목)
f2 = f1; // ERROR !!
f4 = f3; // f3은 문자열을 반환하고, f4는 문자열을 반환할 가능성이 있기 때문에 할당이 가능 (3번 항목)
f3 = f4; // ERROR !! - 그러나 f3(1) 같이 정수가 반환될수 있기 때문에 불가능
Class 타입 호환
클래스 타입은 클래스 타입끼리 비교할 때 스태틱 멤버(static member)와 생성자(constructor)를 제외하고 속성만 비교한다.
class Hulk {
handSize: number;
constructor(name: string, numHand: number) {
this.handSize = numHand;
}
static human: string;
}
class Captain {
handSize: number;
constructor(numHand: number) {
this.handSize = numHand;
}
static human: boolean;
}
let aa: Hulk = new Captain(1000);
console.log('aa: ', aa.handSize); // 1000
let ss: Captain = new Hulk('@', 5000);
console.log('ss: ', ss.handSize); // 5000
Generics 타입 호환
제네릭은 제네릭 타입 간의 호환 여부를 판단할 때, 타입 인자 <T> 가 속성에 할당 되었는지를 기준으로 한다.
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // OK, because y matches structure of x
위 인터페이스는 일단 속성(member 변수)이 없기 때문에 x와 y는 같은 타입으로 간주될 수 있다.
그런데 만약 아래와 같이 인터페이스에 속성이 있어서 제네릭의 타입 인자가 속성에 할당된다면 얘기는 다르게 된다.
인터페이스 NotEmpty에 넘긴 제네릭 타입이 data 속성에도 할당되었으므로, 당연히 인터페이스 프로퍼티 타입 구성이 서로 다르게되니까 x와 y는 서로 다른 타입으로 간주되게 된다.
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // Error, because x and y are not compatible
타입 단언 (Assertions)
타입 단언(단언: 주저하지 아니하고 딱 잘라 말함) 이란 내 손모가지를 걸고 이 타입은 내가 정의할게 맞다고 컴파일러에게 알려주는 것을 말한다.
타입스크립트가 타입 추론을 통해 판단할 수 있는 타입의 범주를 넘는 경우, 더 이상 추론하지 않도록 직접 지시하는 것이다.
C나 JAVA에 있는 타입 형변환(type casting)과 비슷하다고 볼 수 있겠지만, 특별한 검사나 데이터 재구성을 하지 않는다.
즉, 런타임에는 영향을 미치지는 않고 오직 컴파일 과정에서만 사용된다.
타입 단언은 강제 형변환과 다른 개념이다.
강제 형변환은 실제로 데이터 자료를 변환시키지만, 타입 단언은 그냥 타입만 이것이라고 컴파일러에게 주장할 뿐이라 실제 데이터가 바뀌거나 그러지 않는다.
그래서 어찌보면 컴파일러를 속이는 것과 같게 되므로 코드에 빨간줄은 안뜨지만 실제 실행하다보면 오류가 발생할 수 있게 된다.
타입 단언을 선언하는 방법은 크게 두가지가 있다.
1번째 방법은 앵글 브라켓(angle-bracket, <>) 문법을 사용하는 것이다.
let assertion: unknown = "타입 어설션은 '타입을 단언'합니다.";
// 방법 1: assertion 변수의 타입을 string으로 단언 처리
let assertion_count:number = (<string>assertion).length;
2번째 방법은 as 문법을 사용하는 것이다.
let assertion: unknown = "타입 어설션은 '타입을 단언'합니다.";
// 방법 2: assertion 변수의 타입을 string으로 단언 처리
let assertion_count:number = (assertion as string).length;
다만 꺾쇠<> 로 단언하는 방법은, 리액트 JSX/TSX 환경에서 태그 엘리먼트와 문자가 겹칠수 있으며, 제네릭(Generic)과 햇깔릴수 있으니, 왠만하면 as 로 표현하는 것이 추천된다.
다음 예제를 살펴보자.
div 라는 변수에 DOM Element값이 들어온다고 했을때, 타입을 써주지 않으면 컴파일러는 다음과 같이 유니온으로 타입 추론이 하게 되고, null 값이 들어올수 있는 경우를 대비해 div.innerHTML 동작을 에러를 내뿜게 된다.
const div = document.querySelector("div");
div.innerHTML = "asdf";
이때 타입 단언을 해서 내 손모가지를 걸고 변수 div는 무조건 값이 있다고 단언할수 있다.
const div = document.querySelector("div") as HTMLDivElement;
div.innerHTML = "asdf";
혹은 타입스크립트의 느낌표 연산자인 non-null 연산자를 통해 해결이 가능하다.
const div = document.querySelector("div")!;
div.innerHTML = "asdf";
// or
const div = document.querySelector("div");
div!.innerHTML = "asdf";
타입 단언을 맹신하고 마구마구 사용할 경우 여러분의 코드는 지옥을 선사해줄 것이다.
따라서 왠만하면 타입 단언은 해당 변수가 unknown 타입일 경우에만 사용하는 걸 추천하는 바이며, 나머지 상황일때는 이 다음에 배울 타입 가드를 이용해 해결하기를 바란다.
타입 가드 (Guards)
타입 가드는 에러를 줄일수 있는 방어 코드 기법을 의미한다.
타입 가드는 어떠한 전용 문법이 있는 것이 아니라, 타입스크립트에서 우리가 흔히 쓰는 if문 조차도 분기를 잘해서 오류를 최소화 할 수 있다면 그것이 바로 타입 가드 기법이 될 수 있다.
대표적으로 타입 가드에서 사용되는 키워드들이 몇몇 있는데 열거하자면 다음과 같다.
- typeof : 일반 타입 체킹
- instanceof : 클래스 체킹
- Array.isArray() : 배열 체킹
- .type / in : 객체 속성 체킹
이들은 모두 자바스크립트 코드이다.
자바스크립트에서는 타입이 유연하기 때문에 위의 타입 체킹 키워드들을 쓸 일이 별로 없었겠지만, 타입스크립트에서는 정말 자주 보게될 놈들이니 익숙해지자.
일반 타입 가드
예를 들어 아래와 같은 코드가 있다고 하자.
function numOrStr(a: number | string) {
a.toFixed(1); // toFixed 는 number 자료형 메소드이다.
}
toFixed() 메소드는 number 전용 메소드 이기 때문에, 컴파일러 입장에선 아규먼트 a 가 number도 될수있고 string도 될수 있으니, 만일 a가 string이 될 경우 코드가 에러 날 것을 캐치해 저렇게 빨간줄로 우리에게 경고를 하는 것이다.
이때는 간단하게 자바스크립트에서 한번쯤은 써봤던 typeof 키워드를 이용해 조건 분기만 잘 해주면 말끔히 해결된다.
function numOrStr(a: number | string) {
if (typeof a === 'string') {
a.split(',');
}
if (typeof a === 'number') {
a.toFixed(1);
}
}
function numOrStr(a: number | string) {
if (typeof a === 'string') {
a.split(',');
}
else { // else로 간단하게 써도 컴파일러는 이정도는 똑똑하게 구분해준다
a.toFixed(1);
}
}
function numOrStr(a: number | string) {
switch (typeof a) {
case 'string':
a.split(',');
break;
case 'number':
a.toFixed(1);
break;
}
}
if문 뿐만 아니라 switch case 문으로도 타입 가드가 가능하다.
물론 바로 위에서 배운 타입 단언을 통해 강제적으로 타입을 명시해서 빨간줄을 없애는 방법도 있지만, 이는 매우 위험한 행동이지 지양 하여야 한다.
function numOrStr(a: number | string) {
(a as number).toFixed(1);
}
numOrStr(123);
numOrStr('123'); // Error - a.toFixed is not a function
이처럼 앞으로 타입스크립트를 다룰때 여러분들은 위와 같이 union 타입을 만나는 상황이 굉장히 많을텐데, 이때 마다 항상 타입 가드로 방어를 해줘야 하는 것이 중요하다.
배열 타입 가드
function numOrNumArr(a: number | number[]) {
// 배열인지 아닌지
if (Array.isArray(a)) {
a.slice(1);
} else {
a.toFixed(1);
}
}
클래스 타입 가드
타입스크립트 세계관에서는 놀랍게도 클래스 자체도 타입으로 쓰일 수 있다.
대신 클래스 자체가 타입일 경우 타입의 값은 new 클래스() 가 되게 된다.
class Animal {
aaa() {}
}
// 클래스도 타입이 될 수 있다.
function A_B(param: Animal | string) {
if (param instanceof Animal) {
param.aaa();
} else {
console.log(param);
}
}
A_B(new Animal()); // 다만 클래스가 타입일 경우, 데이터 값은 new 생성자가 되게 된다.
A_B('Animal');
객체 타입 가드
만일 다음과 같이 Lion, Ant, Sparrow 리터럴 객체 타입이 있을 경우, 어떻게 이들을 구분 할까?
간단하게 객체의 value가 다른 공통된 속성명을 찾아서 key에 접근해 구분해주면 된다.
이 객체에서 값이 다른 공통된 속성은 type 속성이다. 따라서 타입 가드는 다음과 같이 해주면 된다.
type Lion = { type: 'mammal', gender: 'F' | 'M', bite: boolean };
type Ant = { type: 'insect', gender: 'F' | 'M', acid: boolean };
type Sparrow = { type: 'bird', gender: 'F' | 'M', fly: boolean };
function typeCheck(a: Lion | Ant | Sparrow) {
if (a.type === 'mammal') {
a.bite = true;
} else if (a.type === 'insect') {
a.acid = true;
} else {
a.fly = true;
}
}
그러면 type 속성이 없으면 어떻게 구분할까?
gender 속성은 유니온 이기 때문에 이걸 이용해 타입 가드를 펼치기에는 애로사항이 많다.
그러면 거꾸로, 타입이 같고 속성명이 다른 것을 찾아 가드해주면 된다.
type Lion = { gender: 'F' | 'M', bite: boolean };
type Ant = { gender: 'F' | 'M', acid: boolean };
type Sparrow = { gender: 'F' | 'M', fly: boolean };
function typeCheck(a: Lion | Ant | Sparrow) {
if ('bite' in a) {
a.bite = true;
} else if ('acid' in a) {
a.acid = true;
} else {
a.fly = true;
}
}
코드를 잘보면 in 연산자 가 쓰였음을 알 수 있다.
자바스크립트의 for in 루프문의 in이 바로 이것이다. 우리는 지금까지 for 문으로 in 연산자를 사용해왔지만 사실 in 연산자의 진짜 역할은 객체의 key 값을 꺼내오는 것이다.
따라서 'bite' 라는 문자가 a 객체에 key 속성으로서 쓰임을 검사하면 타입 가드를 할수 있는 것이다.
그래도 이러한 가드 기법이 번거로우면, type 속성 처럼 공통 속성명을 주어 마치 객체에 라벨을 달아놓은 것처럼 가가 객체 타입을 구분하게 미리 타입을 선언해두면 편하게 타입 처리가 가능하다.
인터페이스 타입가드
인터페이스를 타입가드 하는 기법은 위의 객체 타입 가드 하는 기법과 똑같다.
인터페이스에 식별할 수 있는 타입 속성을 기재해주고 .type 으로 검사해주면 된다.
interface Person {
type: 'a';
name: string;
age: number;
}
interface Product {
type: 'b';
name: string;
price: number;
}
function print(value: Person | Product) {
if (value.type === 'a') {
console.log(value.age);
} else {
console.log(value.price);
}
}
이럴때는 보통 if문 보다는 switch문으로 타입 가드를 처리하는 것이 보기 깔끔하다.
interface Person {
type: 'a';
name: string;
age: number;
}
interface Product {
type: 'b';
name: string;
price: number;
}
function print(value: Person | Product) {
switch (value.type) {
case 'a':
console.log(value.age);
break;
case 'b':
console.log(value.price);
break;
}
}
Null 타입 가드
function sample(data: number[] | null) {
// 가장 먼저 data 가 null 인지 검사한다.
// JavaScript 에서 `typeof null == 'object'` 의 결과가 true 이기 때문에, null 여부를 판별할 수 없기 때문이다.
// 따라서 if 조건문에 data 가 유효한 값인지 판단하는 로직을 넣는다
if (data && typeof data === 'object') {
for (const number of data) {
console.log(number);
}
}
}
동등 타입 기법
조금만 생각을 응용해보면 간단한 등호 연산자로도 타입 가드가 가능하다.
function sample(x: string | number, y: string | boolean) {
// 매개변수 x 와 y 가 동일하다면, x 와 y 모두 string 타입이라 추론한다.
if (x === y) {
// x 와 y 모두 string 타입으로 취급한다.
}
}
사용자 정의 타입 가드
간단한 타입 같은 경우는 자바스크립트에서 제공하는 키워드를 이용해 간단히 분기 처리가 가능하다.
하지만 복잡한 타입을 가드할 경우에는 전문적인 타입 가드가 필요한데, 타입스크립트에서는 is 키워드를 통해 타입 가드 전용 함수를 만들 수 있다.
앞서 타입가드는 타입 오류를 최소화하는 하나의 기법이라 설명하였지만, 사실 타입 가드에도 전용 연산자가 존재 한다.
이를 '사용자 정의 타입 가드(User-defined type guards)' 라 칭하며, 이 문법은 함수의 리턴값에 is 연산자를 명시해줌으로서 타입을 확연하게 해주는 헬퍼 함수 같은 역할이라고 보면 된다.
TypeScript 가 타입을 판단하는 방법을 개발자가 직접 정의하거나, 타입을 판단하는 로직을 재사용하고 싶을 때 이용된다.
interface Cat {
meow: number;
}
interface Dog {
bow: number;
}
// 타입 가드 역할을 하는 커스텀 함수
// Dog 타입 인지 확인 하는 역할을 한다. (리턴 타입에 is 키워드를 붙여야 기능함)
function catOrDog(a: Cat | Dog): a is Dog {
// 타입 판별은 여러분이 직접 로직을 구현
if ((a as Cat).meow) {
return false; // 만일 고양이면 false
} else {
return true; // 만일 강아지면 true
}
}
function pet(a: Cat | Dog) {
if (catOrDog(a)) { // 강아지 일 경우
console.log(a.bow);
} else { // 고양이 경우
console.log(a.meow);
}
}
함수 리턴 타입에 is 키워드가 들어가면, 이건 커스텀 타입 가드 전용 함수 라고 치부하면 되며, 이 커스텀 타입 가드 함수는 if문 안에 사용해서 타입을 판별하는 용도로 사용된다고 이해하면 된다.
이렇게만 보면 위의function catOrDog()함수의 리턴값 타입을boolean한것과 무슨 차이가 있느냐고 생각할수도 있겠지만, 실제로a is Dog에서boolean으로 타입을 바꾸면a.bow와a.meow에서 빨간줄이 쳐짐을 확인 할 수 있다.
즉, 사용자 타입 가드 is 키워드는 타입스크립트의 타입을 판별해서 컴파일러에게 알리는 하나의 문법 체계라고 보면 된다.
프로미스 타입 가드
위의 커스텀 타입 가드 함수를 응용해 비동기 api 호출에 실패한 것들을 걸러낼수 있는 응용 타입 가드 로직이다.
const isRejected = (input: PromiseSettledResult<unknown>): input is PromiseRejectedResult {
input.status === 'rejected';
}
const isFulfilled = <T>(input: PromiseSettledResult<T>): input is PromiseFulfilledResult<T> {
input.status === 'fulfilled';
}
const promises = await Promise.allSettled([Promise.resolve('a'), Promise.resolve('b')]);
const errors = promises.filter(isRejected);
# 참고자료
https://heropy.blog/2020/01/27/typescript/
https://typescript-kr.github.io/pages/advanced-types.html
이재승 타입스크립트 시작하기
제로초 타입스크립트 올인원
쉽게 시작하는 타입스크립트 (길벗, 2023, 캡틴판교 지음)
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.