Blog

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

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

5-1. any 타입은 가능한 한 좁은 범위에서만 사용하기

TypeScript 의 타입 시스템은 선택적(Optional) 이고 점진적(Gradual) 이기 때문에 정적이면서도 동적인 특성을 동시에 가진다
any 는 좁은 범위에서 사용해서 한다
예시 코드
  Bad
function f1() { const x: any = expressionReturningFoo(); processBar(x); }
TypeScript
복사
  Good
function f1() { const x = expressionReturningFoo(); processBar(x as any); }
TypeScript
복사
타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에도 함수의 반환 타입을 명시하는 것이 좋다
함수의 반환 타입을 명시하면 any 타입이 함수 바깥으로 영향을 미치는 것을 방지할 수 있다
객체를 선언할 때도 최소한의 범위에서만 any 를 사용해야 한다
예시 코드
 Bad
const config: Config = { a: 1, b: 2, c: { key: value, } } as any;
TypeScript
복사
 Good
const config: Config = { a: 1, b: 2, c: { key: value as any, } }
TypeScript
복사

요약

의도치 않은 타입 안정성의 손실을 피하기 위해서 any 의 사용 범위를 최소한으로 좁혀야 한다
함수의 반환 타입이 any인 경우 타입 안정성이 나빠진다
강제로 타입 오류를 제거하려면 any 대신 @ts-ignore 를 사용해야 한다

5-2. any 를 구체적으로 변형해서 사용하기

일반적인 상황에서 any보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높기 때문에, 구체적인 타입을 찾아 타입 안정성을 높여야 한다
예시 코드
  Bad
function getLengthBad(array: any) { return array.length; }
TypeScript
복사
  Good
함수 내의 array.length 타입 체크
함수 반환 타입이 any 대신 number 로 추론
함수 호출될 때 매개변수가 배열인지 체크
function getLength(array: any[]) { return array.length; }
TypeScript
복사
매개변수가 객체이지만 값을 알 수 없다면 모든 비기본형(non-primitive) 타입을 포함하는 object 타입을 사용할 수 있다
예시 코드
function hasTwelveLetterKey(o: {[key: string]: any}) { // ... } function hasTwelveLetterKey(o: Object) { // ... }
TypeScript
복사

요약

any 를 사용할 때는 정말로 모든 값이 허용되어야하는지 확인해야 한다
any 보다 더 정확하게 모델링할 수 있도록 구체적인 형태를 사용해야 한다

5-3. 함수 안으로 타입 단언문 감추기

함수의 모든 부분을 안전한 타입으로 구현하는 것이 이상적이지만, 불필요한 예외 상황까지 고려해가며 타입 정보를 힘들게 구성할 필요는 없다
함수 내부에는 타입 단언을 사용하고, 함수 외부로 드러나는 타입 정의를 정확히 명시하는 정도로 끝내는것이 좋다

요약

타입 선언문은 일반적으로 타입을 위험하게 만들지, 상황에 따라 필요하기도 하고 현실적인 해결책이 되기도 한다
불가피하게 사용해야 한다면, 정확한 정의를 가지는 함수 안으로 숨겨야 한다

5-4. any 의 진화를 이해하기

타입스크립트에서는 일반적으로 변수의 타입은 변수를 선언할 때 결정된다
그 후에 정제될 수 있지만, 새로운 값이 추가되도록 확장할 수는 없다
any 타입과 관련해서 예외인 경우가 존재한다
예시 코드
function range(start: number, limit: number) { const out = []; for (let i = start; i < limit; i++) { out.push(i); } return out; // 반환 타입이 number[] 로 추론됨 }
TypeScript
복사
any 의 타입 진화(evolve)는 noImplicitAny 가 설정된 상태에서 변수의 타입이 암시적 any 인 경우에만 일어난다
타입의 진화는 값을 할당하거나 배열에 요소를 넣은 후에만 일어나기 때문에, 편집기에서는 이상하게 보일 수 있다
타입을 안전하게 지키기 위해서는 암시적 any 를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 더 좋은 설계이다

요약

일반적인 타입들은 정제되기만 하는 반면, 암시적 any 와 any[] 타입은 진화할 수 있다
any 를 진화시키는 방식보다 명시적 타입 구문을 사용하는것이 안전한 타입을 유지하는 방법이다

5-5. 모르는 타입의 값에는 any 대신 unknown을 사용하기

any 가 강력하면서도 위험한 이유
어떠한 타입이든 any 타입에 할당이 가능하다
any 타입은 어떠한 타입으로도 할당이 가능하다
any 를 사용하면 타입 체커가 무용지물이 된다
unknown 타입은 any의 첫번째 속성을 만족하지만, 두번째 속성은 만족하지 않는다
어떠한 타입이든 unknown 에 할당 가능
unknown 은 오직 unknown 과 any 에만 할당 가능
unknown 타입인 채로 값을 사용하면 오류가 발생하기 때문에 적절한 타입으로 변환하도록 강제할 수 있다
타입 단언문 외에 instanceof, typeof, 사용자 정의 가드 등을 통해서도 원하는 타입으로 변환할 수 있다
예시 코드
function processValue(val: unknown) { if (val instanceof Date) { val // 타입이 Date } }
TypeScript
복사
function isBook(val: unknown): val is Book { return ( typeof(val) === 'object' && val !== null && 'name' in val && 'author' in val ); } function processValue(val: unknown) { if (isBook(val)) { val; // 타입이 Book } }
TypeScript
복사
제네릭을 사용한 스타일은 타입 단언문과 달라 보이지만, 기능적으로는 동일하다
제네릭보다는 unknown 을 반환하고 사용자가 직접 단언문을 사용하거나 원하는 대로 타입을 좁히도록 강제하는 것이 좋다
예시 코드
function safeParseYAML<T>(yaml: string): T { return parseYAML(yaml); }
TypeScript
복사
{} 타입은 null 과 undefined 를 제외한 모든 값을 포함한다
object 타입은 모든 비기본형(non-primitive) 타입으로 이루어진다
unknown 이 도입되기 전에는 {} 가 더 일반적으로 사용되었지만, 최근에는 {} 를 사용하는 경우가 드물다

요약

unknown 은 any 를 대신할 수 있는 안전한 타입이다
어떠한 값이 있지만, 그 타입을 알지 못하는 경우라면 unknown을 사용하면 된다
사용자가 타입 단언문이나 타입 체크를 사용하도록 강제하려면 unknown 을 사용하면 된다

5-6. 몽키 패치보다는 안전한 타입을 사용하기

자바스크립트에서 객체나 클래스에 임의의 속성을 추가할 수 있다
타입스크립트 타입 체커는 글로벌 변수에 내장 속성에 대한 내용은 알지만, 임의로 추가한 속성에 대해서 알지 못하기 때문에 에러가 발생한다
임의로 추가한 속성에 대한 에러를 처리하는 방법
1.
타입 단언문 사용
타입 안정성을 상실하고, 언어 서비스를 사용할 수 없게됨
(document as any).monkey = 'Tamarin';
TypeScript
복사
2.
interface의 특수 기능 중 하나인 보강 (augmentation) 사용
any 보다 더 나은점
타입이 더 안전하다
속성에 주석을 붙일 수 있다
속성에 자동 완성을 사용할 수 있다
몽키 패치가 어떤 부분에 적용되었는지 정확한 기록이 남는다
보강을 사용할 때 주의해야 할 점은 모듈 영역(scope) 와 관련이 있다
보강은 전역적으로 적용되기 때문에, 코드의 다른 부분이나 라이브러리로부터 분리할 수 없다
interface Document { /** 몽키 패치의 속(genus)또는 종(species) */ monkey: string; } document.moneky = 'Tamarin';
TypeScript
복사
export {}; declare global { interface Document { /** 몽키 패치의 속(genus)또는 종(species) */ monkey: string; } }
TypeScript
복사
3.
더 구체적인 타입 단언문 사용
Document 를 확장하기 때문에 타입 단언문은 정상이며, 할당문의 타입은 안전하다
새로운 타입을 도입했기 때문에 모듈 영역 문제도 해결할 수 있다
몽키 패치를 남용해서는 안되며 궁극적으로 더 잘 설계된 구조로 리팩터링하는 것이 좋다
interface MonkeyDocument extends Document { /** 몽키 패치의 속(genus)또는 종(species) */ monkey: string; } (document as MonkeyDocument).monkey = 'Macaque';
TypeScript
복사

요약

전역 변수나 DOM에 데이터를 저장하지 말고, 데이터를 분리하여 사용해야 한다
내장 타입에 데이터를 저장해야 하는 경우, 안전한 타입 접근법 중 하나를 사용해야 한다

5-7. 타입 커버리지를 추적하여 타입 안정성 유지하기

noImplicitAny 를 설정하고 모든 암시적 any 대신 명시적 타입 구문을 추가해도 any 타입과 관련된 문제로부터 안전하다고 할 수 없다
any 타입이 프로그램 내에 존재할 수 있는 경우
1.
명시적 any타입
any 타입의 범위를 좁히고 구체적으로 만들어도 여전히 any 타입이다
특히 any[]{[key: string]: any} 같은 타입은 인덱스를 생성하면 단순 any 가 되고, 코드 전반에 영향을 끼친다
2.
서드파티 타입 선언
a.
@types 선언 파일로부터 any 타입이 전파되기 때문에 특별히 조심해야 한다
b.
noImplicitAny 를 설정하고 절대 any 를 사용하지 않았다 하더라도 여전히 any 타입은 코드 전반에 영향을 끼친다
프로젝트에서 any 의 개수를 추적하는 방법
type-coverage 패키지를 활용하면 any 를 추적할 수 있다
--detail 플래그를 붙이면, any 타입이 있는 곳을 모두 출력해준다
이를 통해서 예상치 못한 any 의 근원지를 찾을 수 있다
$ npx type-coverage 9985 / 10117 98.69%
Shell
복사

요약

noImplicitAny 가 설정되어 있어도, 명시적 any 또는 서드파티 타입 선언을 통해 any 타입은 코드 내에 여전히 존재할 수 있다
작성한 프로그램의 타입이 얼마나 잘 선언되었는지 추적해야 한다