이펙티브 타입스크립트 스터디(5)

오형근·2022년 10월 1일
0

Typescript

목록 보기
12/15
post-thumbnail

Effective Typescript에 대한 스터디 진행 내용을 요약한 것입니다.

WILIM 프로젝트를 진행하느라 스터디를 긴 시간 못했다.
마저 진행해보자!

객체 래퍼 타입 피하기

우리가 알고 있는 Javascript 기본형들에 대한 7가지 타입은 다음과 같다.

  • string
  • number
  • boolean
  • null
  • undefined
    -----------------------------JS 초창기부터 존재
  • symbol
    -----------------------------ES2015에 추가
  • bigInt
    -----------------------------가장 최근에 추가

기본형들은 불변이며, 내장 메서드를 가질 수 없다는 특징을 가진다. 말 그대로 언어 내 가장 기본적인 단위인 것이다.

여기서 JS를 자주 사용해본 사람이라면 뭔가 이상함을 알아챌 수 있을 것이다.

string을 배열처럼 여기고 사용한 메서드들은 뭐지??

맞다...node를 통해 다음 명령어를 시행하면 정상 작동하는 것을 볼 수 있다.

console.log('primitive'.charAt(3));
// 'm' 출력

이는 string타입에는 메서드가 없지만 JS 내에 String이라는 객체 타입이 존재하기 때문이다. JS는 이와 같이 기본형과 객체타입을 자유롭게 변환한다. 이 덕분에 우리가 기본 문자열에도 메서드를 사용할 수 있는 것이다.

string에 char~와 같은 메서드를 사용할 때 JS는 이를 String 객체로 래핑하고 메서드를 실행한 뒤에 감싼 String객체를 버리는 방식으로 연산을 수행한다.

그래서 아래와 같은 일들이 일어난다.

'hello' === new String('hello'); // false

new String('hello') === new String('hello'); // false

혹은 아래와 같은 일이 발생한다.

x = 'hello' // 변수 x 내에 'hello'를 담고
x.language = 'english' // x를 객체로 래핑하여 language라는 값을 넣어줄 수는 있다.
x.language // undefined -> 객체로 래핑하여 값을 넣고 래핑된 객체를 다시 버렸으므로 당연히 undefined가 나온다.

이때 TS에서는 다음과 같은 규칙이 존재한다.

string은 String자리에 들어갈 수 있지만(String의 인스턴스로 간주)
String은 string 자리에 들어갈 수 없다!

이에 대한 예제는 다음과 같다.

function isGreeting(phrase: String) {
return ['hello', 'good day'].includes(phrase) // Argument of type 'String' is not assignable to parameter of type 'string'.

대신 다음은 성립한다.

const a: String | string = new String();
// a 는 String의 인스턴스이므로 String과 string이 모두 가능하다.

예외적으로 bigIntSymbol은 new 생성자 사용이 불가하기 때문에 어디에서나 똑같이 인식이 된다.

잉여 속성 체크의 한계 인지하기

앞에서 배운 구조적 타이핑을 이해하고 있다면 다음 코드가 어렵지 않을 것이다.

interface Room {
    numDoors: number;
    ceilingHeightFt: number;
}

const obj = {
    numDoors: 1,
    ceilingHeightFt: 10,
    elephant: 'present',
}

const r: Room = obj;

//const obj: {
//    numDoors: number;
//    ceilingHeightFt: number;
//    elephant: string;
//}

위 결과 obj의 타입이 잘 확인되는 것을 알 수 있다. r은 Room의 타입을 가지고 있고, obj가 해당 타입의 속성을 모두 만족하고 있으므로 구조적 타이핑이 적용되어 있는 모습이다.

그러나 다음 코드는 그렇지 못하다.

    numDoors: number;
    ceilingHeightFt: number;
}

const obj: Room = {
    numDoors: 1,
    ceilingHeightFt: 10,
    elephant: 'present',
}
// Object literal may only specify known properties, and 'elephant' does not exist in type 'Room'.

const r: Room = obj;

위의 코드처럼 obj에 Room이라는 타입을 지정하게 되면 Room의 속성에 맞지 않는 elephant라는 속성이 충돌을 일으키게 되는 것이다.

다만 이러한 관용(?)은 다음과 같은 극단적인 경우를 통과시키기도 한다...

interface Room {
    numDoors: number;
    ceilingHeightFt: number;
}

interface Options {
    title: string;
    darkMode?: boolean;
}

const o1: Options = document;
const o2: Options = new HTMLAnchorElement();

interface인 Options는 title만 존재한다면 만사 OK이기 때문에 documentHTMLAnchorElement와 같은 title이 존재하는 어떤 객체도 받아들일 수 있게 되는 것이다...

또 재밌는 점은 다음의 코드이다.

interface Room {
    numDoors: number;
    ceilingHeightFt: number;
}

interface Options {
    title: string;
    darkMode?: boolean;
}

const inter1: Options = { darkmode: true, title: 'hello' };
// Object literal may only specify known properties, but 'darkmode' does not exist in type 'Options'. Did you mean to write 'darkMode'?

const inter2 = { darkmode: true, title : 'hello' };
const a: Options = inter2;

위의 타입 선언과 같은 경우 darkmode를 오타로 인식하고 친절하게 darkMode를 사용할 것을 말해주고 있다.

그러나 아래의 경우 그런 것 없이 darkmode를 다른 속성이라고 생각하고 받아들이고 있다...여기에서도 타입 단언보다 타입 선언이 사용되어야 하는 이유를 볼 수 있는 것 같다.

마지막으로 아래 예제를 살펴보자.

interface LineChartOptions {
    logscale?: boolean;
    invertedYAxis?: boolean;
    areaChart?: boolean;
}

const opts = { logScale: true };
const a: LineChartOptions = opts;
// Type '{ logScale: boolean; }' has no properties in common with type 'LineChartOptions'.

여기에서는 LineChartOptions라는 인터페이스가 모두 선택적 속성을 제공하고 있음에도 a는 오류를 발생시킨다. 이는 optsLineChartOptions와 공통으로 가지는 속성이 단 하나도 없어서 발생하는 것으로, 다음과 같이 고칠 수 있다.

interface LineChartOptions {
    logscale?: boolean;
    invertedYAxis?: boolean;
    areaChart?: boolean;
}

const opts = { logScale: true, logscale: true };
const a: LineChartOptions = opts;

logScale을 오타 수정이 아닌 완전히 다른 속성으로 간주하는 것이다.

이러한 경우 에러를 없애려면 선택적 속성만 가진 인터페이스라도 겹치는 속성이 하나라도 존재하도록 선언해야한다.

interface LineChartOptions {
    logscale?: boolean;
    invertedYAxis?: boolean;
    areaChart?: boolean;
}

const opts = { logscale: true, title: true };
const a: LineChartOptions = opts;

위와 같은 코드는 성립한다는 이야기이다. 또한, opts객체 내에 아무것도 없는 경우도 에러를 발생시키지 않는다.

이처럼 타입 단언과 비슷한 방식으로 타입을 선언하게 되면 확장에 유연한 객체가 생성되지만, 그만큼 엄격하지 못한 객체가 생성되는 것이라고 생각할 수 있다.

상황에 맞게 선언과 단언을 선택하여 사용하도록 하자..!

0개의 댓글