반응형
아이템 51 - 의존성 분리를 위해 미러 타입 사용하기
- 각자 필요한 모듈만 사용할 수 있도록 구조적 타이핑을 적용
- 즉, 의존성을 분리해 사용자가 사용에 용이하도록 함
- 작성중인 라이브러리가 의존하는 라이브러리의 구현과 무관하게 타입에만 의존한다면, 필요한 선언부만 추출하여 작성 중인 라이브러리에 넣는 것(미러링)을 고려해볼 수 있음
- 다른 라이브러리의 타입 선언 대부분을 추출해야 한다면, 차라리 명시적으로
@types
의존성을 추가하는 것이 나음 - 유닛 테스트와 상용 시스템 간의 의존성을 분리하는데도 유용
- 다른 라이브러리의 타입 선언 대부분을 추출해야 한다면, 차라리 명시적으로
아이템 52 - 테스팅 타입의 함정에 주의하기
헬퍼 함수를 통한 테스팅
// lodash의 map
import { map } from "lodash";
// 매개변수, 반환 타입 모두 체크 가능
function assertType<T>(x: T) {}
assertType<number[]>(map(["john", "paul"], (name) => name.length));
타입을 테스트할 때는 특히 함수 타입의 동일성과 할당 가능성의 차이점을 알고 있어야 함 (구조적 타이핑)
const n = 12;
assertType<number>(n); // 정상
const beatles = ["john", "paul", "george", "ringo"];
assertType<{ name: string }[]>(
map(beatles, (name) => ({
name,
inYellowSubmarine: name === "ringo",
}))
); // 정상
콜백이 있는 함수를 테스트할 때 콜백 매개변수의 추론된 타입을 체크해야 함
// 선언된 것보다 적은 매개변수를 가진 함수를 할당하는 것은 아무런 문제가 없다는 것을 보여줌
const g: (x: string, y: number) => any = (z: string) => 12; //정상
const g: (x: string) => any = () => 12; //정상
// 로대시의 map함수의 콜백 매개변수 세 개를 모두 사용하는 경우는 매우 드묾
map(array, (name, index, array) => {
/* ... */
});
제대로 된 assertType 사용 방법
// Parameters와 ReturnType 제네릭 타입을 이용
const double = (x: number) => 2 * x;
let p: Parameters<typeof double> = null!;
assertType<[number, number]>(p); // 오류
// ~ '[number]' 형식의 인수는 '[number, number]'
// 형식의 매개변수에 할당될 수 없음
let r: ReturnType<typeof double> = null!;
assertType<number>(r); // 정상
this가 API의 일부분이라면 역시 테스트해야 함
const beatles = ["john", "paul", "george", "ringo"];
assertType<number[]>(
map(beatles, function (name, i, array) {
// ~~~ '(name: any, i: any, array: any) => any' 형식의 인수는
// '(u: string) => any' 형식의 매개변수에 할당될 수 없음
assertType<string>(name);
assertType<number>(i);
assertType<string[]>(array);
assertType<string[]>(this);
// ~~~ 'this'에는 암시적으로 'any' 형식이 포함
return name.length;
})
);
declare function map<U, V>(
array: U[],
fn: (this: U[], u: U, i: number, array: U[]) => V
): V[];
타입 관련된 테스트에서 any를 주의해야하고, 더 엄격한 테스트를 위해 dtslint같은 도구를 활용하는 것이 좋음
- 타입 시스템 내에서 암시적 any 타입을 발견해 내는 것은 매우 어려움
- dtslint 는 할당 가능성이 아닌 심벌 타입을 추출해 글자 자체가 같은지 비교
const beatles = ["join", "paul", "george", "ringo"];
map(
beatles,
function (
name, // $ExpectType string
i, // $ExpectType number
array // $ExpectType string[]
) {
this; // $ExpectType string[]
return name.length;
}
); // $ExcpectType number[]
아이템 53 - 타입스크립트 기능보다는 ECMAScript 기능을 사용하기
- 타입 스크립트 팀은 TC39는 런타임 기능을 발전시키고, 타입스크립트 팀은 타입 기능만 발전시킨다는 명확한 원칙을 세우고 지켜오고 있음
- 해당 원착이 세워지기 전에, 이미 사용되고 있던 몇 가지 기능이 있는데, 타입 공간과 값 공간의 경계를 혼란스럽게 만들기 때문에 사용하지 않는 것이 좋음
피해야하는 기능
- 열거형(enum)
- 숫자 열거형은 할당한 숫자 이외의 숫자가 할당되면 매우 위험
- 상수 열거형은 보통의 열거형과 달리 런타임에 완전히 제거됨
preserveConstEnums
플래그를 설정한 상태의 상수 열거형은 보통의 열거형처럼 런타임 코드에 상수 열거형 정보를 유지함- 문자열 열거형은 런타임의 타입 안정성과 투명성을 제공하나 구조적 타이핑이 아닌 명목적 타이핑을 사용
- 문자열 열거형 대신 리터럴 타입의 유니온 사용
- 매개변수 속성
- 컴파일을 하면 코드가 늘어남
- 런타임에는 실제로 사용되지만, 타입스크립트 관점에서는 사용되지 않는 것 처럼 보임
- 일반 속성과 섞어 사용하면 설계가 혼란스러워짐
- 네임스페이스와 트리플 슬래시 임포트
module
사용 금지
- 데코레이터
- 표준이 아니기 때문에 사용하지 않는 것이 좋음
experimentalDecorators
속성을 작성하고 사용해야 함
아이템 54 - 객체를 순회하는 노하우
const obj = {
one: 'uno',
two: 'dos',
three: 'tres'
}
for (const k in obj) {
const v = obj[k]; // obj에 인덱스 시그니처가 없기 때문에, 암시적 'any' 타입
}
let k: keyof typeof obj;
for (k in obj) {
const v = obj[k] // 해결
}
- 타입 문제 없이 객체의 키와 값을 순회하고 싶을 때
interface ABC {
a: string;
b: string;
c: number;
}
function foo(abc: ABC) {
for (const [k, v] of Object.entries(abc)) {
k // string 타입
v // any 타입
}
}
- 객체를 다룰 때 '프로토타입 오염'의 가능성을 염두에 두어야 함
for in
구문을 사용하면, 객체의 정의에 없는 속성이 등장할 수 있음
결론
- 객체를 순회하며 키와 값을 얻는 방법
let k: keyof T
와 같은keyof
선언 - 상수나 정확한 타입에 적절Object.entries
사용 - 일반적이나, 키와 값의 타입을 다루기 까다로움
아이템 55 - DOM 계층 구조 이해하기
계층 구조별 타입
- EventTarget
- DOM 타입 중 가장 추상화된 타입
- 이벤트 리스너를 추가, 제거, 이벤트 전송
function handleDrag(eDown: Event) {
const targetEl = eDown.currentTarget;
targetEl.classList.add('dragging') // 개체가 'null'인 것 같습니다.
// 'EventTarget' 형식에 'classList' 속성이 없습니다.
}
- Event의 currentTarget 속성의 타입은
EventTarget | null
- 타입 관점에서
window
나XMLHttpRequest
가 될 수도 있음
- Node 타입
- 텍스트 조각, 주석
children
-HTMLCollection
childNodes
-NodeList
, 텍스트 조각과 주석도 포함HTMLxxxElement
- 자신만의 고유한 속성을 가지기 때문에, 구체적으로 타입을 지정해야 함
- 단언문을 사용해야 할 경우가 많음
null
체크를 해줘야 함
아이템 56 - 정보를 감추는 목적으로 private 사용하지 않기
public
,protected
,private
같은 접근 제어자는 타입스크립트 키워드이기 때문에 컴파일 후에 제거됨- 즉, 런타임에서 효력이 없음
정보를 숨기는 방법
- 클로저 사용
- 비공개 필드 기능 (표준화 진행중)
- 접두사로
#
을 붙여 타입 체크와 런타임 모두에서 비공개로 사용 - 외부 접근 불가, 개별 인스턴스끼리는 접근 가능
WeakMap
을 사용해 구현된 기능
- 접두사로
아이템 57 - 소스맵을 사용하여 타입스크립트 디버깅하기
tsconfig.json
에서sourceMap
옵션 설정을 통해 사용- 원본 코드가 아닌 변환된 자바스크립트를 디버깅하는것이 아니라, 소스맵을 사용해 런타임의 타입스크립트 코드를 디버깅해야 함
- 소스맵이 최종적으로 변환된 코드에 완전히 매핑되었는지 확인해야 함
- 소스맵에 원본 코드가 공개되지 않도록 설정해야 함
아이템 58 - 모던 자바스크립트로 작성하기
- ECMAScript 모듈 사용하기
- commonJS 대신 ESModule (import, export)
- 프로토타입 대신 클래스 사용하기
- 문법이 간결하며 직관적임
var
대신let
/const
사용하기- 스코프 문제를 피할 수 있음
- 호이스팅 문제를 피할 수 있음
for(;;)
대신for-of
또는 배열 메서드 사용하기for in
문법은 몇 가지 문제점 때문에 사용하지 않는 것이 좋음
- 함수 표현식보다 화살표 함수 사용하기
noImplicitThis
를 설정해this
바인딩 관련 오류를 표시해주는 것이 좋음
- 단축 객체 표현과 구조 분해 할당 사용
- 함수 매개변수 기본값 사용하기
- 저수준 프로미스나 콜백 대신
async
/await
사용하기 - 연관 배열에 객체 대신
Map
과Set
사용하기 - 타입스크립트에
use strict
넣지 않기alwaysStrict
사용하기
아이템 59 - 타입스크립트 도입 전에 @ts-check
와 JSDoc
으로 시험해 보기
@ts-check
지시자를 사용해 타입 체커가 파일을 분석하고, 발견된 오류를 보고하도록 지시- 매우 느슨한 수준으로 타입 체크를 수행함을 유의
- 선언되지 않은 전역변수
- 변수를 제대로 인식할 수 있게 별도로 타입 선언 파일을 만들어야 함
- 선언 파일을 찾지 못하는 경우 '트리플 슬래시' 참조
/// <reference path="./types.d.ts" />
- 알 수 없는 라이브러리
- 서드파티 라이브러리들의 타입 선언을 활용하여 타입 체크를 시험해 볼 수 있음
- DOM 문제
- 타입 단언문 대체
- 부정확한 JSDoc
@ts-check
와 같이 사용하면 오류가 발생할 수 있음
아이템 60 - allowJS로 타입스크립트와 자바스크립트 같이 사용하기
- 점진적 마이그레이션을 위해 자바스크립트와 타입스크립트를 동시에 사용할 수 있게
allowJS
컴파일러 옵션을 사용 - 마이그레이션 작업 전에, 테스트와 빌드 체인에 타입스크립트 적용
아이템 61 - 의존성 관계에 따라 모듈 단위로 전환하기
- 다른 모듈에 의존하지 않는 최하단 모듈부터 작업을 시작
- 마이그레이션과 동시에 리팩토링은 지양
전환시 나타나는 오류들
- 선언되지 않은 클래스 멤버
- 자바스크립트는 클래스 멤버 변수를 선언할 필요가 없지만, 타입스크립트에서는 명시적으로 선언해야 함
- 멤버 변수를 선언하지 않은 클래스가 있는 js파일을 ts파일로 바꾸면, 참조하는 속성마다 오류가 발생
- 타입이 바뀌는 값
- 객체 선언 시 한번에 선언 or 임시방편으로 타입 단언문 사용
@ts-check
, JSDoc 작동하지 않음
아이템 62 - 마이그레이션의 완성을 위해 noImplicitAny 설정하기
noImplicitAny
설정을 활성화해 마이그레이션의 마지막 단계를 진행해야 함- 로컬에서부터 타입 오류를 점진적으로 수정해야 함
출처: 이펙티브 타입스크립트