이펙티브 타입스크립트- 3장 타입추론 (5)
아이템 26. 타입 추론에 문맥이 어떻게 사용되는지 이해하기
타입스크립트는 타입을 추론할 때 단순히 값만 고려하지 않고 값이 존재하는 곳의 문맥까지 살핀다.
그런데 문맥을 고려해 타입을 추론하면 가끔 값이 이상하게 나온다.
function setLanguage(language) {
/*...*/
}
setLanguage('JavaScript'); // 정상
let language = 'JavaScript';
setLanguage(language); // 정상
type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) {
/*...*/
}
setLanguage('JavaScript'); // 정상
//인라인 형태에서 타입스크립트는 매개변수가 Language 타입이어야 함을 알고 있음
//그러나 이 값을 변수로 분리하면 할당 시점에 타입을 추론함.
let language = 'JavaScript';
setLanguage(language); // error Argument of type 'string' is not assignable to parameter of type 'Language'.
let Language로 변수로 분리하면서 type Langauage에 할당 될 수 있지만 아이템 21에 의하면 가장 넓은 타입을 기본으로 가지기 때문에 language는 string 타입으로 설정된다.
type Language는 string을 부분집합으로 가지지 않기 때문에 타입 체킹에 오류가 발생한다.
이를 해결하기 위해서는 아래와 같이 타입을 좁히거나 상수 선언 해준다.
//타입좁히기
let language: Language = 'JavaScript'; //type: Language
setLanguage(language) // 정상
//상수선언
const language = 'JavaScript'; // type: `JavaScript`
setLanguage(language) // 정상
두 번째 예시의 경우 const를 사용하여 타입체커에게 languages는 변경할 수 없다고 알려줌. 따라서 타입스크립트는 language에
대해 더 정확한 타입입 문자열 리터럴(`JavaScript`)로 추론할 수 있다.
하지만 이 과정에서 사용되는 문맥으로 부터 값을 분리 했다. 문맥과 값을 분리하면 추후에 근본적인 문제를 발생시킬 수 있는데 이러한 문맥의 소실로 인해 오류 케이스와 해결방법을 알아보자.
1. 튜플 사용 시 주의점
function panTo(where: [number, number]) {
/*...*/
}
panTo([10, 20]); // 정상
const loc = [10, 20]; // type: number[];
panTo(loc); //error [number, number] != number[];
//타입 선언을 사용하는 방법
const loc: [number, number] = [10, 20];
panTo(loc); // 정상
//상수 문맥을 제공하는 방법
const loc = [10, 20] as const;
panTo(loc); //error [number, number] != readonly [10, 20];
function panTo(where: readonly [number, number]) {
/*...*/
}
const 로 선언한 객체나 number의 경우 내부 속성값을 수정할수 있기 때문에 가장 확장된 타입으로 추론된다. 따라서 위와같이 타입을 변경해야 할 필요가 있다
function panTo(where: readonly [number, number]) {
/*...*/
}
const loc = [10, 20, 30] as const;
panTo(loc); //error readonly [number, number] != readonly [10, 20, 30]; length is diffrent;
위 코드에서의 문제는 함수가 잘못된 것이 아니라 변수선언이 잘못 된 형태이다. 다만 error를 발생한 지점은 함수를 호출한 지점으로 위처럼 단순하게 선언되있다면 찾기가 쉽지만 여러겹으로 중첩된 객체의경우 근본적으로 원인파악이 가능한 곳을 찾기가 어려워질 수 있다.
2. 객체 사용 시 주의점
3. 콜백 사용 시 주의점
function callWithRandomNumbers(fn: (n1: number, n2: number) => void) {
fn(Math.random().Math.random());
}
callWithRandomNumbers((a, b) => {
a // type이 number
b // type이 number
console.log(a + b);
}); // 정상
const fn = (a, b) => {
// type error : noImplicitAny
console.log(a + b);
};
callWithRandomNumbers(fn);
콜백을 상수로 뽑아내면 문맥이 소실되고 noImplicitAny 에러가 뜬다.
이런 경우 매개변수에 타입 구문을 추가해서 해결할 수 있다.
const fn = (a:number, b:number) => {
console.log(a + b);
};
callWithRandomNumbers(fn);
정리하자면 콜백 함수를 파라미터에 직접 추가하면 자동적으로 타입 추론이 되지만 일반 변수로 뽑아 사용할때는 콜백함수의 파라미터 값이 자동추론이 되지 않고 any타입으로 추론된다. 함수 표현식으로 타입을 선언하여 재사용하자.
요약
- 타입 추론에 문맥이 어떻게 쓰이는지 주의해서 살펴봐야 된다.
- 변수를 뽑아서 별도로 선언했을 때 오류가 발생한다면 타입 선언을 추가해야 한다.
- 변숙가 정말로 상수라면 상수 단언(as const)를 사용해야 한다. 그러나 상수 단언을 사용하면 정의한 곳이 아니라
사용한 곳에서 오류가 발생하므로 주의해야 한다.
아이템 27. 함수형 기법과 라이브러리로 타입 흐름을 유지하기
파이썬, C, 자바 등에서 볼 수 있는 표준 라이브러리가 자바스크립트에는 포함되어 있지 않다.
하지만 수년간 제이쿼리, 언더스코어, 로대시, 람다 등의 많은 라이브러리들은 표준 라이브러리의 역할을 대신하기 위해 노력해왔다.
이러한 라이브러리들의 일부 기능(map, flatMap, filter, reduce)은 순수 자바스크립트로 구현되어 있고 이러한 기법은 루프를 대체할 수 있기 때문에 유용하게 사용된다. 이때 타입스크립트와 조합하여 사용하면 더욱 빛을 발한다.
그 이유는 타입 정보가 그대로 유지되면서 타입 흐름(flow)이 계속 전달되록 하기 때문이다.
반면 직접 루프를 구현하면 타입 체크에 대한 관리도 직접 해야 한다.
아래는 CSV 데이터를 파싱한다고 해보자. 절차형 프로그래밍에서 함수형 프로그래밍으로 코드 라인을 줄일 수 있다.
// 절차형 프로그래밍
const csvData = '...';
const rawRows = csvData.split('\n');
const headers = rawRows[0].split(',');
const rows = rawRows.slice(1).map(rowStr => {
const row = {};
rowStr.split(',').forEach((val, j) => {
row[headers[j]] = val;
});
return row
});
// 함수형 프로그래밍
const rows = rawRows.slice(1)
.map(rowStr => rowStr.split(',').reduce((row,val,i) => (row[headers[i]] = val, row), {}));
이 전 코드에 비해 세 줄 절약했지만 더 복잡하게 느껴질 수 있다.
이때, 키와 값 배열로 취합(zipping)해서 객체로 만들어주는 로대시의 zipObject 함수를 이용하면 좋다.
// Lodash 사용
import '_' from 'lodash';
const rows = rawRows.slice(1).map(rowStr => _.zipObject(headers, rowStr.split(',')));
그런데 자바스크립트에서 서드파티 라이브러리 종속성을 추가할 때는 코드를 짧게 줄이는 데 시간이 많이 든다면 사용하지 않는 게 낫기 때문에 신중해야 한다.
그러나 같은 코드를 타입스크립트로 작성하면 서드파티 라이브러리를 사용하는 것이 유리하다.
그 이유는 타입 정보를 참고하며 작업할 수 있기 때문이다.
아래의 예시에서 CSV파서의 절차형 버전과 함수형 버전 모두 같은 오류를 발생 시킨다.
// 절차형
const rows = rawRows.slice(1).map((rowStr) => {
const row = {};
rowStr.split(',').forEach((val, j) => {
row[headers[j]] = val;
// No index signature with a parameter of type 'string' was found on type '{}'
});
return row;
});
// 함수형
const rows = rawRows.slice(1).map((rowStr) =>
rowStr.split(',').reduce(
(row, val, i) => (
(row[headers[i]] = val),
// No index signature with a parameter of type 'string' was found on type '{}'
row
),
{}
)
);
두 버전 모두 {}의 타입으로 {[column:string]: string} 또는 Record<string, string>을 제공하면 오류가 해결된다.
한편 로대시 버전은 별도의 수정 없이도 타입 체커를 통과한다.
이처럼 타입을 설정하지 않아도 통과할 만큼 잘 되있고 타입지정이 정확하게 되어 있다는 점이 서드파티를 사용하는데 장점이 크다.
타입스크립트를 사용하면서 타입을 작성하고 고민할 시간이 훨씬 줄어든다.
요약
- 타입 흐름을 개선하고 가독성을 높이고 명시적인 타입 구문의 필요성을 줄이기 위해 직접 구현하기 보다
내장된 함수형 기법돠 로대시 같은 유틸리티 라이브러리를 사용하는 것이 좋다.