우리는 앞서 TS에서 특정 변수가 어떤 타입을 가져야하는지를 어떻게 TSC에게 명시시켜주는지 여러 방법들에 대해서 알아봤다. 지금부터는 그런 문법들과 함께, 타입들이 TS에서는 어떻게 작성되는지 살펴보자. 앞 포스팅에서 살펴봤던 JS의 타입들을 최대한 그대로 가져다 쓰면서, override하는 방식으로, 몇 가지가 더 추가되어 있는 형태라고 생각하면 쉽다.
해당 포스팅의 주요 내용들은 TypeScript의 공식 문서 Handbook의 EverydayTypes 부분을 바탕으로 작성되었다.
TS에서의 원시 타입은 JS에서 가장 흔하게 사용되는 세 가지 원시 타입인 String, Number, Boolean를 분류하고 있다고 칭한다. JS에서는 그와 함께 Null, Undefined, Bigint, Symbol도 원시 타입으로 분류하고 있다. 이러한 분류에 따라서 사용법이 달라지는 것은 없으니 해당 글에서는 총 일곱가지 원시 타입을 짧게 분류해보기로 하자. 원시 타입들은 TS에서 각자 대응하는 타입이 존재한다. 또한 TS의 원시타입은 JS에서 typeof 연산자를 통해 얻을 수 있는 것과 동일한 이름을 가지고 있다. typeof 연산자는 JS에서 런타임 내의 타입 감지를 가능하게 하는데, 반환값으로 object, undefined, boolean 등의 string값을 가진다.
typeof 함수 표준 문서
즉, TS에서는 원시 타입의 사용을 일반적으로 ‘소문자’로 시작하는 값으로 활용한다는 것을 기억하자.
string: 문자열 값들의 타입을 말한다.
const value = "Hello, world!";
console.log(typeof value);
// "string"
number: 숫자 값들의 타입을 말한다.
const value = 1;
console.log(typeof value);
// "number"
boolean: 논리 값들의 타입을 말한다. true, false값만을 가질 수 있다.
const value = true;
console.log(typeof value);
// "boolean"
undefined: 선언되었지만 값이 할당되지 않았거나, 메서드와 선언 또는 함수의 반환에서 변수가 값을 할당받지 못했을 때 해당 변수의 타입을 말한다.
const value = {} as {"a" : string};
console.log(typeof value.a);
// "undefined"
null: 의도적으로 값을 가지지 않는 변수의 타입을 말한다.
null 값의 경우는 typeof를 사용하였을 때 반환값이 object인데, 이는 JS에서 구현 오류로 취급하고 있다. 물론 TS에서는 해당 타입을 null로 인식하고 있다.
const value:null = null;
console.log(typeof value);
// "object"
bigint: 큰 수를 정의하기 위해 사용되는 타입이다.
const value = 123n;
console.log(typeof value);
// "bigint"
symbol: 고유한 객체 키의 생성을 위해 사용되는 변수의 타입이다.
심볼의 경우 해당 심볼을 구분하기 위한 특정 값을 생성자 내부에 입력하여 생성하는데, 같은 값으로 심볼을 생성하여도 서로 다른 고유 값을 가지게 된다. 또한, JS의 object는 키 값으로 string과 symbol만을 사용할 수 있도록 하고 있다. 따라서 사용자가 임의로 참조하지 못하는 특정한 객체의 값을 private하게 생성하기 위해 사용되는 변수 타입이며, 특정 값에 심볼 값을 담아서 사용하지 않고 바로 객체에서 심볼키에 대한 값을 선언하여 할당할 경우 해당 키를 어떻게 해서도 참조할 수 없게 된다.
const value = Symbol("symbol");
console.log(typeof value);
// "symbol"
Symbol("value") === Symbol("value")
// false
const obj:{[key:symbol] : string} = {};
obj[value] = "value";
console.log(obj);
// {Symbol(symbol):"value"}
const value2 = Symbol("symbol");
console.log(obj[value2])
// undefined
원시 타입들 사이에서는 아래와 같은 몇 가지 재미있는 주제들이 생겨난다. 해당 글에서 이러한 부분까지 깊게 풀어내기에는 주제에서 벗어나는 것 같으니, 궁금할 경우 직접 찾아보도록 하자. 그래도 내가 생각하는 짧은 요약과 참고할만한 레퍼런스를 포함해본다.
1.1 + 0.1 == 1.2
// false
1234567890123456789 * 123
// 151851850485185200000 ==> 정수 표현 범위를 넘어선 number 연산
1234567890123456789n * 123n;
// 151851850485185185047n ==> bigint 자료형 활용(숫자 뒤에 n 붙히면 bigint)
보통 숫자값을 저장하기 위해 사용되는 변수 타입인 number는 부동소수점 표기를 활용하여 값을 표기하기 때문에, 가지고 있는 데이터 사이즈에 비해서 정수값을 표현하려면 그 범위의 한계가 명확해진다. 또한 정수의 경우에는 해당하지 않지만 부동소수점 표기의 경우에는 비교연산이 쉽지 않다. 이러한 특성들로 인해 JS에서는 bigint를 정수 타입으로 사용할 수 있도록 구현하였다. bigint type의 경우 ES2020 표준에 추가된 타입이기 때문에 그보다 낮은 스크립트 버전에서는 해당 타입을 사용할 수 없다.
관련 추천 블로그 글: javascript-Number-vs-BigInt
undefined는 의도하지 않은 비어있는 값이고, null은 의도된 비어있는 값이라고 생각하면 쉽다.
관련 추천 블로그 글1: null과 undefined
관련 추천 블로그 글2: undefined와 null의 차이점을 설명하세요
이제 우리는 위에서 배운 타입들과 앞서 배운 타입 추정 방법들을 활용해서 특정 변수 혹은 함수의 타입을 TSC에게 가르쳐줄 수 있다. 하지만 우리는 한 가지 변수가 한 가지 타입만을 가진다고 확신할 수 없으며, object type의 경우에는 여러 가지 타입들이 value에 다양하게 활용될 수 있다. 따라서 type을 커스텀할 수 있어야 하는데, 지금부터는 사용자 설정 타입들을 어떻게 작성하고 조합하는지 알아보도록 하자.
TS의 컨셉상 변수의 타입을 한정지어야 하는 것은 이해하겠는데, string이나 number 같은 타입을 변수 하나에 하나씩 지정하는 것은 너무 피곤한 일일 것이다. 그래서 TS에서는 한 가지 변수가 여러 가지 타입을 가질 수 있도록 하기 위하여 type union을 통해서 이를 명시할 수 있도록 하였다.
let strOrNum: string | number = 5
strOrNum = 'hi'
위와 같이 union type을 사용하여 타입을 정의하면, 여러 타입 중 한 가지만 만족하는 타입을 가지면 TSC가 오류를 반환하지 않는다.
object는 JavaScript 개발환경에서 단연코 가장 많이 쓰이는 데이터 형태일 것이다. 대부분의 웹 통신 프레임워크들의 표준은 json이고, RESTful API로 통신하는 경우에도 대부분의 데이터 형태를 object로 활용한다. 웹 개발을 하다보면 심지어는 array보다 object가 익숙하다고 느끼는 순간이 올 때도 있다.
그만큼 object는 굉장히 자주 사용되기 때문에 수많은 오류의 원인이 된다. 특히 값의 초기화가 완료되지 않았거나 하여서 접근하려는 특정 key가 undefined일 경우의 런타임 에러가 굉장히 일어나기 쉽다. TS에서는 이를 방지하기 위하여 여러 가지 대책을 마련해두었다.
일단 특정 object 타입을 선언하는 것은 다음과 같은 방법으로 이루어진다.
const user: { name: string, age: number } = { name: 'jihan', age: 29 }
user.age = '25'
// Type 'string' is not assignable to type 'number'.
user.email = 'abc123@gmail.com'
// Property 'email' does not exist on type '{ name: string; age: number; }'.
특정 key 값의 타입을 지정해주면, 할당 시에 value 값의 타입을 검사해준다. 또한, key값으로 선언한 적 없는 임의의 key에 값을 할당해주려 하면, 선언된 타입 내에 해당하는 키가 없다고 에러를 뱉어낸다.
물론 우리는 key 값이 임의로 부여될 수 있도록 구성해야할 때도 있을 것이다. 이럴 때는 다음과 같은 문법을 사용한다.
const user: { [key: string]: string | number } = { name: 'jihan', age: 29 }
user.age = '25'
user.email = 'abc123@gmail.com'
console.log(user)
// { "name": "jihan", "age": "25", "email": "abc123@gmail.com" }
object 타입의 key가 될 수 있는 것은 string과 symbol 뿐이다.
근데 변수를 선언할 때마다 union type과 object type 등 여러 가지를 계속해서 선언해주는 것은 너무 번거롭다고 느낀다. 이제 타입을 선언하기 위한 예약어를 두 가지 살펴볼 것이다. ‘type’과 ‘interface’이다. 타입 예약어는 특정 타입 값을 저장해 놓고 이를 불러오기 위해서, 타입에 이름을 붙여줄 때 사용된다. 아래는 가장 기초적인 사용 방법이다.
type myType1 = string | number;
type myType2 = {
key1: string;
key2: number | boolean;
}
const myVar1: myType1 = 'hi'
const myVar2: myType2 = {
key1: 'hi',
key2: 1,
}
type Animal = {
name: string
}
type Bear = Animal & {
honey: Boolean
} // type 예약어의 타입 확장
const bear = getBear();
bear.name;
bear.honey;
interface myInterface {
key1: string;
key2: number | boolean;
}
const myVar: myInterface = {
key1: 'hi',
key2: 1,
}
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
} // interface 예약어의 타입 확장
const bear = getBear()
bear.name
bear.honey
타입 예약어와 인터페이스 예약어는 얼핏 보기에 차이가 없어 보인다. 그냥 단순히 문법이 조금 다른 정도라고 느낄 수 있다.
물론 두 가지의 차이가 있다. 이 두 가지를 알고 나면 interface의 이름이 왜 interface인지 어렴풋이 알 것도 같으면서 필요에 의해 구분해서 사용해야할 때가 있다는 것을 알 수 있다.
interface myType string
// 'string' only refers to a type, but is being used as a value here.
// '{' expected.
type myType = string
interface userType {
name: string;
}
interface userType {
age: number;
}
const user: userType = {
name: 'jihan',
age: 29,
}
type userType = {
name: string;
}
// Duplicate identifier 'userType'.
type userType = {
age: number;
}
// Duplicate identifier 'userType'.
const user: userType = {
name: 'jihan',
age: 29,
}
// Object literal may only specify known properties, and 'age' does not exist in type 'userType'.
애초에 type문을 사용할 경우, 같은 이름의 타입에 대한 중복 선언이 불가하다. 하지만 interface의 경우, 중복하여 선언문을 작성하는 것으로 object에 key를 추가하는 것이 가능한데, 이는 모듈화가 일상인 JS 생태계에서 굉장히 큰 강점으로 활용된다. 버전 관리를 하기 위해 낮은 버전의 코드를 하위호환할 수 있도록 하면서 새로운 버전의 코드를 작성하여 추가할 때 변수 명은 그대로 하며 type에 새로운 key를 추가하는 등으로 이를 활용할 수 있다.다음 편에서는 해당 편에서 다루지 못했던 TS에서 지원하는 다양하고 특이한 정의된 타입들에 대해서 다뤄보자. any와 unknown을 포함하여, 함수의 타입을 지정하는 방법도 함께 공부해보도록 하자.