책정리/Effective Typescript

이펙티브 타입스크립트 5장 - any 타입 다루기

뽀글보리 2024. 1. 1. 10:53
반응형

5장 any 다루기

타입스크립트의 타입 시스템은 선택적이고 점진적이기 때문에 정적이면서도 동적인 특성을 동시에 가진다. 프로그램의 일부분에만 타입 시스템을 적용할 수 있다는 특성떄문에 자바스크립트 코드를 타입스크립트로 점진적인 마이그레이션이 가능하다. 이 때, any를 사용하여 일부분의 타입 체크를 비활성화시켜줄 수 있다. 하지만 any는 매우 강력하므로 남용하게 될 소지가 높다.

 

아이템 38 any 타입은 가능한 한 좁은 범위에서만 사용하기

1) 매개변수 내에서만 any 사용하기

x: any = returningFoo() // 1
processBar(x)

x = returningFoo() // 2
processBar(x as any)

 

1보다는 2를 사용한다. any 타입이 매개변수에서만 사용되기 때문에, 다른 코드들에는 영향이 가지 않는다.

 

2) 함수 반환 타입으로 any 사용하지 않기

function f1() {
	const x: any = returningFoo();
    processSomething(x);
    return x; // 타입이 any
}

 

함수에서 any를 반환하지 않는 게 좋다. any를 정의하면 영향력이 프로젝트 전반에 퍼지게 될 수 있으니, 함수 바깥으로 영향을 미치지 않도록 하는 것이 좋다.

 

3) ts-ignore를 사용하기 보다 근본적인 문제점 해결하기

ts-ignore를 사용하면, any를 사용하지 않고도 오류를 제거할 수 있다.

그러나, 타입 체커가 알려주는 오류는 문제가 될 가능성이 높은 부분이므로 근본적인 원인을 찾아 적극적으로 대처하는 것이 바람직하다

 

4) 객체를 any로 선언하기 보다 속성 일부만 any 사용하기

const config: Config: {
  a: 1,
  b: 2,
  c: {
    key: value
  }
 } as any // 객체 전체에 any
 
 const config: Config = {
   a: 1,
   b: 2,
   c: {
     key: value as any
   }
 } // 속성의 일부에만 any

 

객체 전체를 any로 선언하지 말고 속성 일부만 최소한으로 사용하자

 

아이템 39 any를 구체적으로 변형해서 사용하기

1) 배열임을 알 수 있는 경우

  • 배열임을 알 수 있는 경우, any보다 any[]타입을 사용할 수 있다.
  • 배열의 배열 형태라면? any[][]로 선언할 수 있다.
  • array.length 타입이 체크된다.
  • 함수 호출 시 매개변수가 배열인지 체크된다.

2) 객체이긴 하지만 값을 알 수 없는 경우

  • {[key: string]: any}로 타입을 선언할 수 있다.
    • object 타입은 객체의 키를 열거할 수는 있지만 속성에 접근할 수 없다는 점에서 다르다.

3) 매개변수의 개수를 구체화하기

  • 함수일 경우에도 매개변수 개수 정도는 대략적으로 구체화할 수 있다.
    • 매개변수가 존재하지 않는 경우 () ⇒ any
    • 매개변수가 1개 존재하는 경우 (arg: any) ⇒ any
    • 매개변수가 2개 이상인 경우 (…args: any[]) ⇒ any

아이템 40 함수 안으로 타입 단언문 감추기

함수의 모든 부분을 안전한 타입으로 구현하는 것이 이상적이지만, 불필요한 예외 상황까지 고려해 가며 타입 정보를 힘들게 구성할 필요는 없다. 함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의를 정확히 명시하는 정도로 끝내는 게 낫다. 프로젝트 전반에 위험한 타입 단언문이 드러나 있는 것보다 제대로 타입이 정의된 함수 안으로 타입 단언문을 감추는 것이 더 좋은 설계이다.

객체가 같은지 체크하기 위해 객체 순회와 단언문이 코드에 직접 들어가는 것보다, 앞의 코드처럼 별도의 함수로 분리해 내는 것이 훨씬 좋은 설계이다.

 

아이템 41 any의 진화를 이해하기

1) any 타입은 진화할 수 있다.

const result = []; // any[]
result.push(10) // number[]
result.push('a') // (number|string)[]

 

  • 처음에 any[]로 선언되었지만 number타입의 값을 넣는 순간 number[]로 진화(evolve)한다.
  • any가 진화하는 방식은 일반적인 변수가 추론되는 원리와 동일하다.
  • 타입 좁히기(아이템 22)와는 반대되는 개념으로, string을 넣으면 (string | number)[]로 확장되며 진화된다.

2) 명시적으로 any를 선언하면 진화하지 않는다.

const result: any[] = [];
result.push('a');
result.puah(1);
console.log(result) // any[]
  • any 타입의 진화는 타입스크립트 설정인 noImplicitAny가 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어난다.
  • 명시적으로 any를 선언하면 타입이 그대로 유지된다.

3) 함수 호출을 통해서는 진화하지 않는다.

const result = [] // any

range(start, limit).forEach((i => {
  result.push(i); // number 타입의 값을 push
})

console.log(result) // any

 

암시적 any는 함수 호출을 거쳐도 진화하지 않는다.

 

4) any 타입의 진화보다는 명시적 타입 구문을 사용하는 것이 더 좋다.

number만 넣어야 하는 타입에 string을 넣어서 잘못 진화한 타입을 만들 수도 있다.

⇒ 타입을 안전하게 지키기 위해서는 암시적 any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 더 좋은 설계이다.

 

아이템 42 모르는 타입의 값에는 any 대신에 unknown을 사용하기

unknown은 타입 시스템에 부합하는 타입, any는 그렇지 못한 타입

 

any는 강력하면서도 위험하다

  • 어떠한 타입이든 any 타입에 할당 가능하다
  • any 타입은 어떠한 타입으로도 할당 가능하다

한 집합은 다른 모든 집합의 부분 집합이면서 동시에 상위집합이 될 수 없기 때문에 타입 시스템과 상충되는 면이 있다.

 

unknown은 any 대신 쓸 수 있는 타입 시스템에 부합하는 타입이다. 어떠한 타입이든 unknown에 할당 가능하지만, unknown은 오직 unknown, any에만 할당 가능하다.

 

타입이 있지만 그 타입을 모르는 경우에 any 대신에 unknown 사용하는 것이 좋다. 예를 들어 JSON 직렬화 같은 경우가 있다.

 

unknown에서 원하는 타입으로 변환하는 방법

  1. instanceof를 체크한 후 원하는 타입으로 변환
  2. 사용자 정의 타입 가드 target is Developer
  3. 제네릭 매개변수 대신 사용
    • 타입 단언문과 달라 보이지만 기능적으로는 동일하다.
    • 제네릭보다는 unknwon반환하고 직접 단언문을 사용하거나 타입을 좁히도록 강제하는 것이 좋다.

as any보다 as unknown을 사용하기

 

타입 이중 단언문을 사용할 때, as any보다 as unknown을 사용하는 게 좋다.

as any as Bar 
as unknwon as Bar

 

기능적으로는 동일하다. 그러나, 나중에 단언문을 분리하는 리팩터링을 한다면 unknown 형태가 더 안전하다. any의 경우는 분리되는 순간 그 영향력이 전염병처럼 퍼지게 된다. unknown의 경우에는 분리되는 즉시 오류를 발생하게 되므로 더 안전하다.

 

{}, object, unknown의 차이점

{}타입은 null과 undefined를 제외한 모든 값을 포함한다.

object 타입은 모든 비기본형 타입으로 이루어진다. 객체와 배열은 포함된다.

정말 null과 undefined가 불가능하다고 판단되는 경우에만 unknown 대신 {}를 사용한다.

 

아이템 43 몽키 패치보다는 안전한 타입을 사용하기

자바스크립트는 임의의 속성을 객체나 클래스에 추가할 수 있다.

예시) window.monkey = ‘Tamarin’

내장 기능의 프로토타입에도 속성을 추가할 수 있다.

그러나 전역 변수를 사용하면 side effect이 있을 수 있다.

 

1) 인터페이스 보강 사용하기

interface Document {
  monkey: string;
}

document.monkey = 'Tamarin'; // 정상적으로 보강됨

 

(document as any).monkey를 사용하지 않고 인터페이스 보강(interface augmentation)기법을 사용하여 속성을 추가할 수 있다. 보강 기법을 사용하면 다음과 같은 장점이 있다.

  1. 타입 체크를 할 수 있다.
  2. 주석을 붙이거나 자동완성을 사용 가능하다.
  3. 몽키패치가 어떤 부분에 적용되었는 지 정확한 기록이 남는다.

모듈 영역 문제

  1. 보강은 전역적으로 적용되기 때문에, 코드의 다른 부분이나 라이브러리로부터 분리할 수 없다.
  2. 런타임에 속성을 할당하면, 어떤 엘리먼트는 속성이 있고, 어떤 엘리먼트는 속성이 없는 문제가 발생할 수 있다.

2) 더 구체적인 타입 단언문을 사용한다.

interface MonkeyDocument extends Document { monkey: string; }

(document as MonkeyDocument).monkey = 'Tarmarin'; 
  1. 새로운 타입을 도입했기 때문애 모듈 영역 문제를 해결할 수 있다.
  2. 그러나 몽키패치를 남용해서는 안되고, 더 잘 설계된 구조로 리팩터링하는 게 더 좋다.

아이템 44 타입 커버리지를 추적하여 타입 안전성 유지하기

타입스크립트에 noImplicitAny 설정하고, 명시적 타입 구문을 추가하여도 여전히 any 타입은 프로그램 내에 존재할 수 있다. 다음과 같은 경우가 있을 수 있다.

  1. 명시적 any 타입
    • any[][]로 구체화시켰다고 해도 여전히 any 타입이다. 이는 코드 전반에 영향을 준다.
  2. 서드파티 타입 선언
    • @types 선언 파일로부터 any 타입이 전파되기 때문에 특별히 조심해야 한다.

프로그램 전반에서 사용되는 any를 추적하기 위해서 npx type-coverage를 사용할 수 있다.

이를 사용하면 타입 커버리지를 확인하여 any가 남아있는 퍼센티지를 추적할 수 있다.

주기적으로 코드를 점검하며 타입 안전성을 꾸준히 높일 수 있도록 해야한다.

반응형

'책정리 > Effective Typescript' 카테고리의 다른 글

Quiz로 정리하는 타입스크립트 문법 (1)  (2) 2023.11.24