Front-end Developer

0%

타입스크립트 이펙티브 아이템 49 - 아이템 55

아이템 49 콜백에서 this에 대한 타입 제공하기

this는 let,const와 달리 dynamic scope이기 때문에 정의된 방식이 아닌 호출된 방식에 따라 값이 달라진다. 아래 코드는 현재의 객체 인스턴스를 참조하는 클래스에서 많이 쓰이는 this이다.

1
2
3
4
5
6
7
8
9
10
11
class C {
vals = [1, 2, 3]
logSquares() {
for (const val of this.vals) {
console.log(val * val)
}
}
}
const c = new C()
const method = c.logSquares
method()

이 코드는 런타임에 uncaught TypeError: undefined의 ‘vals’속성을 읽을 수 없습니다.는 오류를 발생시킨다. 그 이유는 c.logSquaresC.prototype.logSquares를 호출하고, this 값을 c로 바인딩하는 두 가지 작업을 수행하기 때문이다. 이 작업에 따르면 this의 값은 undefined로 설정된다. 이런 문제를 해결하는 첫 번째 방법은 call을 사용해서 명시적으로 this를 바인딩하는 것이다.

1
2
3
const c = new C()
const method = c.logSquares
method.call(c) // 제곱 출력

이처럼 명시적인 this 바인딩을 통해 this의 타입에 대해 구체화할 수 있다. this 바인딩은 어떤 것에도 가능하며, 콜백 함수에도 쓰일 수 있다.

1
2
3
4
5
6
7
8
9
declare function makeButton(props: { text: string; onClick: () => void }): void
class ResetButton {
render() {
return makeButton({ text: 'Reset', onClick: this.onClick })
}
onClick() {
alert(`Reset ${this}`)
}
}

그런데 여기서도 this 바인딩 문제로 인해 Reset이 정의되지 않았다는 에러가 발생한다. 이때 해결하는 방법은 아래와 같이 생성자에서 메서드에 this를 바인딩시키는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
declare function makeButton(props: { text: string; onClick: () => void }): void
class ResetButton {
constructor() {
this.onClick = this.onClick.bind(this)
}
render() {
return makeButton({ text: 'Reset', onClick: this.onClick })
}
onClick() {
alert(`Reset ${this}`)
}
}

onClick()은 ResetButton.proptotype의 속성을 정의한다. 따라서 ResetButton의 모든 인스턴스에 공유된다. 그런데 위와 같이 생성자 함수에 바인딩하게 되면 onClick 속성에 this가 바인딩되어 해당 인스턴스에 생성된다. onClick 인스턴스 속성은 프로토타입 속성보다 앞에 놓이므로 render() 메서드의 this.onClick은 바인딩된 함수를 참조하게 된다. 하지만 onClick을 화살표 함수로 바꾸는 방법으로 좀 더 간단하게 해결할 수도 있다. 그 이유는 화살표 함수 내부에서 this를 참조하면 상위 컨텍스트의 this를 그대로 참조하기 때문이다. 따라서 ResetButton이 생성될 때 마다 제대로 바인딩된 this를 가지는 새 함수를 생성한다.


아이템 50 오버로딩 타입보다는 조건부 타입을 사용하기

1
2
3
4
5
6
7
8
function double(x: number | string): number | string
function double(x: any) {
return x + x
}

function double(x) {
return x + x
}

함수 오버로딩(동일한 이름에 매개변수만 다른 여러 버전의 함수를 허용하는 것.단, 타입 수준에서 동작)의 개념을 통해 위와 같이 함수의 타입 정보를 추가한다. 그런데 이렇게 작성하면 선언문에서 number타입을 매개변수로 넣고, string타입을 반환하는 경우도 포함되어 있어서 모호한 지점이 생긴다. 이런 상황에서 첫 번째 해결법은 제너릭을 사용하는 것이다.

1
2
3
4
5
6
7
function double<T extends number | string>(x: T): T
function double(x: any) {
return x + x
}

const num = double(12) // Type is 12
const str = double('x') // Type is "x"

제너릭 타입을 쓰면서 앞서 말했던 number로 선언되어 string 타입을 반환하는 경우에 대해서 해결은 되었으나 지나치게 구체적인 타입이 되었다. string을 넣으면 string만 반환해야 하게 되었다. 좀 더 다른 방법으로 여러 가지 타입 선언으로 분리해 볼 수 있다. 함수의 구현체는 하나여도 타입 선언은 여러 개 만들 수 있기 때문에 함수 타입을 보다 명확하게 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function double(x: number): number
function double(x: string): string
function double(x: any) {
return x + x
}

const num = double(12) // Type is number
const str = double('x') // Type is string
function f(x: number | string) {
return double(x)
// ~ Argument of type 'string | number' is not assignable
// to parameter of type 'string'
}

단 유니온 타입에 대해서는 문제가 발생한다. 타입스크립트는 오버로딩 타입 중에서 일치하는 타입을 찾을 때까지 순차적으로 검색한다. 마지막 선언인 string까지 검색했을 때 string | number 타입은 string에 할당할 수 없기 때문에 에러가 발생하는 것이다. 이때는 오버로딩 타입으로 string|number를 추가하여 문제를 해결할 수 있고, 더 좋은 방법은 아래와 같이 조건부 타입을 사용하는 것이다.

1
2
3
4
5
6
function double<T extends number | string>(
x: T
): T extends string ? string : number
function double(x: any) {
return x + x
}

조건부 타입을 사용하면 아래와 같이 반환 타입이 정교해진다.

  • T가 string의 부분 집합이면 반환 타입이 string이다.
  • 그 외의 경우는 반환 타입이 number이다.

이처럼 조건부 타입은 개별 타입의 유니온으로 일반화하기 때문에 타입이 더욱 정확해진다. 조건부 타입은 타입 체커가 단일 표현식으로 받아들이기 때문에 타입 오버로딩에서 유니온 문제가 발생했을 때 문제를 해결할 수 있다.


아이템 51 의존성 분리를 위해 미러 타입 사용하기

1
2
3
4
5
6
7
8
9
function parseCSV(contents: string | Buffer): { [column: string]: string }[] {
if (typeof contents === 'object') {
// It's a buffer
return parseCSV(contents.toString('utf8'))
}
// COMPRESS
return []
// END
}

다음 코드는 NodeJs 사용자를 위해 매개변수에 Buffer 타입을 허용한 예시이다. 그리고 Buffer의 타입 정의는 npm install —save-dev @types/node로 설치하여 얻을 수 있다. 단 타입 선언이 @types/node에 의존하기 때문에 devDependencies로 포함해야 하는데 이를 포함하면 @types와 무관한 자바스크립트 개발자 또는 NodeJS와 무관한 타입스크립트 웹 개발자에게 혼란을 줄 수 있다. 그 이유는 두 그룹 사용자가 사용하지 않는 모듈이 포함되어 있기 때문이다. Buffer는 NodeJS 개발자에게만 필요하다. 따라서 Buffer를 사용하는 대신 아래와 같이 필요한 메서드와 속성만 별도로 작성하는 방식으로 개선할 수 있다.

1
2
3
4
5
6
7
8
9
10
interface CsvBuffer {
toString(encoding: string): string
}
function parseCSV(
contents: string | CsvBuffer
): { [column: string]: string }[] {
// COMPRESS
return []
// END
}

만약 작성 중인 라이브러리가 의존하는 라이브러리의 구현과 무관하게 타입에만 의존한다면, 필요한 선언부만 추출해서 작성 중인 라이브러리에 넣는 미러링을 고려해 볼 수 있다.


아이템 52 테스팅 타입의 함정에 주의하기

타입 선언도 테스트를 거쳐야 하고, dtslint 또는 타입 시스템 외부의 타입을 검사하는 도구를 사용할 수 있다. 타입 선언이 예상한 타입으로 결과를 내는지 체크할 수 있는 한 가지 방법은 함수를 호출하는 테스트 파일의 작성이다. 그러나 단순히 함수를 실행만 하는 테스트 코드를 작성하는 것보다는 반환 타입을 체크하는 것이 중요하다. 반환값을 특정 타입의 변수에 할당하여 간단히 반환 타입을 체크할 수 있는 방법은 다음과 같다.

1
const lengths: number[] = map(['john', 'paul'], name => name.length)

이 코드는 불필요한 타입 선언에 해당하지만 테스트 관점에서는 매우 중요하다. 그런데 테스팅을 위해서 할당하는 방법에는 두 가지 문제가 있다.

  • 불필요한 변수를 만들어야 하고, 일부 린팅 규칙을 비활성화해야한다.

    • 변수를 도입하는 대신 헬퍼 함수를 정의하는 것으로 해결한다.
    1
    2
    3
    function assertType<T>(x: T) {}

    assertType<number[]>(map(['john', 'paul'], name => name.length))
  • 두 타입이 동일한지에 대한 체크 대신 할당 가능성을 체크한다.

1
2
const n = 12
assertType<number>(n) //정상

n 심벌은 타입이 숫자 리터럴 12인데, 12는 number의 서브타입이라서 할당 가능성 체크를 통과한다. 그러나 객체 타입 체크해서 문제가 발생한다.

1
2
3
4
5
6
7
const beatles = ['john', 'paul', 'george', 'ringo']
assertType<{ name: string }[]>(
map(beatles, name => ({
name,
inYellowSubmarine: name === 'ringo'
}))
) // OK

map은 {name: string, inYellowSubmarine: boolean}객체의 배열을 반환하는데, 반환된 배열은 {name: string}[]에 할당 가능하지만 inYellowSubmarine 속성에 대해 체크되지 않는다.

assertType 사용 방법

아래 예제처럼 parameters와 ReturnType 제너릭 타입을 이용해서 함수의 매개변수 타입과 반환 타입만 분리하여 테스트한다.

1
2
3
4
5
6
7
const double = (x: number) => 2 * x
let p: Parameters<typeof double> = null!
assertType<[number, number]>(p)
// ~ Argument of type '[number]' is not
// assignable to parameter of type [number, number]
let r: ReturnType<typeof double> = null!
assertType<number>(r) // OK

아이템 53 타입스크립트 기능보다는 ECMAScript 기능을 사용하기

타입스크립트 초기 버전에는 자바스크립트가 가진 결함들도 수용해야 했기 때문에 독립적으로 개발한 클래스, 열거형, 모듈 시스템을 포함시켰다. 시간이 흐르면서 부족했던 부분들을 내장 기능으로 추가하게 되었는데 자바스크립트에 새로 추가된 기능은 타입스크립트 초기 버전에서 독립적으로 개발했던 기능과 호환성 문제를 발생시킨다. 그래서 타입스크립트는 자바스크립트의 신규 기능을 그대로 채택하고 타입스크립트 초기 버전과 호환성을 포기하는 방법을 택했다. 그런데 이 기능과 타입스크립트 팀은 타입만 발전시킨다는 원칙이 세워지기 이전에 이미 사용되던 몇 가지 기능들이 있고, 이런 것들이 타입 공간(타입스크립트), 값 공간(자바스크립트)의 경계를 혼란스럽게 하기 때문에 사용을 지양해야 한다.


References
[이펙티브 타입스크립트] 댄 밴더캄 지음