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

이펙티브 타입스크립트 북스터디 5장 아이템 38-44

타입스크립트의 타입 시스템은 선택적(optional)이고 점진적(gradual)이다. 즉 다른 강타입 언어들과 달리 타입 시스템을 적용할지 안 할지 선택할 수 있고, 프로그램의 일부분에만 타입 시스템을 적용했다가 점점 적용범위를 늘려갈 수도 있다. 따라서 점진적인 마이그래이션(JavaScript → TypeScript)이 가능하다.

이때 any 타입은 타입 체크를 비활성화 시키는 등 타입 시스템에서 벗어나게 해주는 특징이 있기 때문에 마이그레이션에서 중요한 역할을 한다.

//any 는 타입 스크립트의 보호장치(타입 체커)을 무력화 시킨다.
const a: any[] = [1, 2, 3];
const b: any = true;

a + b;  //에러가 발생하지 않음!

5장에서는 any 타입을 남용하지 않고 잘 활용하는 방법을 알려주고 있다.


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

요약

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

설명

  1. any의 사용 범위를 최소한으로 좁히자.

    • any 타입이 강력한 이유
      • 어떠한 타입이든 any 타입에 할당 가능하다.
      • any 타입은 어떠한 타입에도 할당 가능하다.
      • 이러한 특징 때문에 any를 사용하면 타입 에러가 안난다. 이는 any의 강력함의 원천이면서 동시에 문제 원인이 되므로 any가 영향을 미치는 범위를 좁게 만들어 줘야 한다.
    • any 타입이 필요할 경우, 변수 타입에 any를 명시하지 말고, 함수 인자로 넘길 때만 타입 단언을 사용하자.
    type Foo = {
        foo: string;
    };
    type Bar = {
        bar: string;
    };
    const expressionReturningFoo = (): Foo => {
        return {
            foo: "foo",
        };
    };
    const processBar = (x: Bar): void => {
        console.log(x);
    };
    
    function f1() {
        const x: any = expressionReturningFoo(); //❌
        processBar(x);
        //any 타입이 된 변수 x가 다른 코드에도 영향을 미친다.
        //또한 이 x가 return 된다면 any 타입이 코드 여기저기 퍼져버리는 상황이 생기게 된다.
    }
    function f2() {
        const x = expressionReturningFoo();
        processBar(x as any); //better! ✅
        //processBar 함수의 매개변수에만 사용된 표현식이므로 다른 코드에 영향X
    }
    • 객체에서 내부 속성에서 타입 에러가 나면 그 객체 전체가 아니라 에러 나는 부분에만 any 타입 단언을 사용하자.
    interface Config {
        a: number,
        b: number,
        c: string
    }
    
    const value = "hi";
    
    const config: Config = {
        a: 1,
        b: 2,
        c: {
            key: value
        }
    } as any; //❌
    
    const config: Config = {
        a: 1,
        b: 2,
        c: {
            key: value
        } as any; //✅
    }
  2. 함수의 반환 타입이 any인 경우 타입 안정성이 나빠지므로 any 타입을 반환하면 절대 안 된다.

    • 함수가 any를 반환하면 그 영향력은 프로젝트 전반에 전염병처럼 퍼지게 된다.
    function f3() {
        const x: any = expressionReturningFoo(); 
        processBar(x);
        return x; //❌
        //any 타입을 절대 반환하지 말자!
    }
    
    function g() {
        const foo = f3(); //타입이 any
    }
    • any 타입이 생각없이 반환되는 것을 막기 위해 함수의 반환 타입을 명시하자. → any 타입 반환 방지
    function f3(): string {  //반환 타입 명시
        const x: any = expressionReturningFoo(); 
        processBar(x);
        return x;
    }
    
    function g() {
        const foo = f3(); //타입이 string
    }
  3. 강제로 타입 오류를 제거하려면 any 대신 @ts-ignore를 사용하는게 좋다.

    • @ts-ignore : 다음 줄의 오류 무시.
    • 해당 타입 오류를 잠깐 건너뛰고 싶을 때 any 로 바꿔주는 것보다는 @ts-ignore가 낫지만, 어디까지나 임시방편임으로 에러가 생기면 적극적으로 대처하는게 바람직하다.
    function f4() {
        const x = expressionReturningFoo();
        //@ts-ignore
        processBar(x);  //에러 무시
        return x;
    }

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

요약

  • any를 사용할 때는 정말로 모든 값이 허용되어야만 하는지 면밀히 검토해야 한다.
  • any보다 더 정확하게 모델링 할 수 있도록 any[] 또는 {[id: string]: any} 또는 () => any처럼 구체적인 형태를 사용해야한다.

설명

  1. any를 사용할 때는 정말로 모든 값이 허용되어야만 하는지 면밀히 검토해야 한다.

    • any 타입은 모든 숫자, 문자열, 배열, 객체, 정규식, 함수, 클래스, DOM 엘레먼트, null, undefined 까지 포함한다.

      ⇒ 일반적인 상황에서 any 보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높다.

    const numArgsBad = (...args: any) => args.length; //❌ return 타입이 any
    const numArgsGood = (...args: any[]) => args.length; //✅ return 타입이 number
    
    /**
    후자가 더 좋은 이유
    1. args.length 타입이 체크 됨
    2. 함수 반환 타입이 number 로 추론 됨
    3. 함수 호출 시, 매개변수가 배열인지 체크 됨
    */
  2. any보다 더 정확하게 모델링 할 수 있도록 any[] 또는 {[id: string]: any} 또는 () => any처럼 구체적인 형태를 사용해야한다. (그냥 any 보다는 조금이라도 더 구체화시키려 노력하자.)


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

요약

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

설명

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

    • 가능한 안전한 타입으로 구현하는 것이 이상적이나, 불필요한 예외 상황까지 고려해 가며 타입 정보를 힘들게 구성할 필요는 없다.
    • 함수 내부에서는 타입 단언 을 사용하고, 함수 외부로 드러나는 타입 정의를 명시하는 정도가 적절할 수 있다.
    //예제 - 함수 캐싱하는 함수: 함수가 자신의 마지막 호출을 캐시(기억)하도록 만들어주는 함수
    declare function shallowEqual(a: any, b: any): boolean;
    function cacheLast<T extends Function>(fn: T): T {
        let lastArgs: any[] | null = null;
        let lastResult: any;
    
        return function(...args: any[]) {
            if (!lastArgs || !shallowEqual(lastArgs, args)) {
                lastResult = fn(...args);
                lastArgs = args;
            }
            return lastResult;
        } as unknown as T;
    }
    
    //원본 함수 타입 T와 리턴하는 함수가 어떤 관련이 있는지 모르기 때문에 에러 발생!
    //그려나 우리는 두 함수가 '같은 매개변수를 주면 같은 값을 반환하는' 함수여서 
    //동일하게 취급해도 문제 없다는 것을 알기때문에 단언문 사용해도 괜찮다.

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

요약

  • 일반적인 타입들은 정제(타입좁히기 item22 와 같은 동작)되기만 하는 반면, 암시적 any와 any[] 타입은 진화할 수 있다. 이러한 동작이 발생하는 코드를 인지하고 이해할 수 있어야 한다.
  • any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법이다.

설명

  1. 일반적인 타입들은 정제(타입좁히기와 같은 동작)되기만 하는 반면, 암시적 any와 any[] 타입은 진화할 수 있다. 이러한 동작이 발생하는 코드를 인지하고 이해할 수 있어야 한다.

    • ❗️타입의 진화는 값을 할당하거나 배열의 요소를 넣은 ‘후’에만 일어나기때문에, 편집기에서는 할당 다음 줄을 봐야 진화된 타입이 잡힌다.
    //1. 배열의 any 타입 진화(evolve)
    const result = [];  //any[]
    result.push('a');  
    result   //string[]
    result.push(1);
    result   //(string | number)[]
    //2. 조건문에 따른 any 타입 진화(evolve)
    let val;  //any
    if(Math.random() < 0.5) {
        val = /hello/;
        val  //RegExp
    } else {
        val = 12;
        val  //number
    }
    
    val  //number | RegExp
    //3. 초깃값이 null 인 경우 any 타입 진화(evolve)
    let tmp = null;  //any
    try {
        somethingDangerous();
        tmp = 12;
        tmp  //number
    } catch (e) {
        console.log('err!');
    }
    
    tmp  //number | null
    • any 타입의 진화는 "noImplicitAny": true 로 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어난다. → 명시적인 경우 진화가 일어나지 않는다.
    //명시적인 경우 진화가 일어나지 않는다.
    let val2: any;  //any
    if(Math.random() < 0.5) {
        val2 = /hello/;
        val2  //any
    } else {
        val2 = 12;
        val2  //any
    }
    
    val2  //any
  2. any를 진화시키는 방식보다 명시적 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법이다.

    • 암시적 any 상태인 변수에 어떠한 할당 없이 사용하려 하면 암시적 any 오류가 발생한다.
    function range(start: number, limit: number) {
        const out = []; //암시적 any
        if (start === limit) {
            return out; //❌ Error:'out' 변수에는 암시적으로 'any[]' 형식이 포함됩니다.
        }
    
        for(let i = start; i < limit; i++) {
            out.push(i);
        }
        return out;  // number[] 로 추론되기 때문에 에러 없음.
    }
    • 암시적 any 타입은 함수 호출을 거쳐도 진화하지 않는다.
    function makeSqure(start: number, limit: number) {
        const out = []; //❌ ''out' 변수는 형식을 확인할 수 없는 경우 일부 위치에서 암시적으로 'any[]' 형식입니다.
        range(start, limit).forEach(i => {
            out.push(i * i);
        })
        return out; 
    }
    • 의도치 않은 타입이 섞여서 잘못 진화할 수 있기 때문에, (암시적 any 진화 방식보단) 명시적 타입 구문 사용이 더 좋은 설계다.
    const result = [];  //any[]
    result.push(1);
    result   //number[]
    
    //보다는 처음부터 명시
    const result: number[] = [];

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

요약

  • unknown은 any 대신 사용할 수 있는 안전한 타입이다. 어떠한 값이 있지만 그 타입을 알지 못하는 경우라면 unknown을 사용함으로써 타입 단언문이나 타입 체크를 사용하도록 강제할 수 있다. (any 일 때 타입 체크가 안 되는 문제점이 보완된다.)
  • unknown는 모든 타입의 상위 타입이고, never는 모든 타입의 하위 타입이다. unknown 타입은 모든 타입이 될 수 있으나 모든 타입은 unknown이 될 수 없다. 반대로 never 타입은 모든 타입이 될 수 없고 모든 타입은 never가 될 수 있다.
  • {} 타입은 null과 undefinedfmf 제외한 모든 값을 포함한다.

설명

  1. 어떠한 값이 있지만 그 타입을 알지 못하는 경우라면 any 대신 unknown을 사용하자.

    • unknown을 사용함으로써 타입 단언문이나 타입 체크를 사용하도록 강제할 수 있다. (any 일 때 타입 체크가 안 되는 문제점이 보완된다.)
    • unknown 은 unknown 인 채로 사용하면 오류가 발생한다.
    interface Book {
        name: string,
        author: string
    }
    
    //❌ 반환값이 any 여서 타입 체크가 안되어 런타임 에러가 발생한다.
    const parseYamL = (yaml: string): any => {};
    
    const book = parseYamL(`name: Jane 
                            author: Char`);
                            
    //parseYamL를 호출한 곳에서 타입 선언이나 타입 단언을 하지 않으면 book이 any가 된다.
    //따라서 아래 코드는 타입 에러도 나지 않고 런타임에서 에러가 난다.
    console.log(book.title);
    book();
    
    //✅ 대신 unknown을 쓰자.
    const safetyParseYamL = (text: string): unknown => ({});
    
    const book2 = safetyParseYamL(`name: Jane 
                                    author: Char`);
    //unknown은 타입에러가 난다! 런타임 단계가 아니라 컴파일 단계에서 에러 확인 가능하다.
    
    console.log(book2.title); // 'book2'은(는) 'unknown' 형식입니다.
    book2(); // 'book2'은(는) 'unknown' 형식입니다.
    • 이중 단언문에서도 any 대신 unknown 을 사용할 수도 있다.
    declare const foo: Foo;
    
    let barAny = foo as any as Bar;
    let barUnk = foo as unknown as Bar;
  2. unkown는 모든 타입의 상위 타입이고, never는 모든 타입의 하위 타입이다.

    • 타입의 값을 집합으로 생각하기(아이템7)를 참고해보자.
    • any 타입은 모든 타입에 할당 될 수도 있고, 모든 타입이 any에 할당 될 수 있으니, 타입 시스템의 열외로 본다. 하지만 unknown은 타입 시스템에 부합한다.
    • any를 제외한 일반적인 타입은 타입 좁히기(더 작은 집합 되기)만 가능하다. 따라서 unknown 타입은 모든 타입이 될 수 있으나 모든 타입은 unknown이 될 수 없다. 반대로 never 타입은 모든 타입이 될 수 없고 모든 타입은 never가 될 수 있다.
    type-venn-diagram
    let num: number;
    
    function somethingDo(): unknown {
        return;
    }
    
    num = somethingDo(); //❌ 'unknown' 형식은 'number' 형식에 할당할 수 없습니다.
    
    function hello(): never {
        throw new Error("xxx");
    }
    
    num = hello();  //✅
    
    let imNever =  hello();
    imNever = 12;  //❌ 'number' 형식은 'never' 형식에 할당할 수 없습니다
  3. {} 타입은 null과 undefined를 제외한 모든 값을 포함한다.

    • unknown 타입이 도입되기 이전에는 {}가 일반적으로 사용되었다. 최근에는 잘 사용하지 않는다.
    • 정말로 null과 undefined가 불가능하다고 판단되는 경우에만 {}를 대신 사용해야 한다.
    • object 타입은 모든 비기본형(non-primitive) 타입으로 이루어진다. ex) 객체, 배열

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

요약

  • 몽키패치란, 원래 소스코드를 변경하지 않고 실행 시 코드 기본 동작을 추가, 변경 또는 억제하는 기술을 의미한다. 자바스크립트에 있어서는 프로토타입에 특정 메소드 추가 한다거나 document 객체에 전역 변수를 삽입하는 등이 있다.
  • 전역 변수나 DOM에 데이터를 저장하는 것보다 데이터를 분리하여 사용해야 한다.
  • 어쩔수 없이 내장 타입에 데이터를 저장해야 하는 경우, 안전한 타입 접근법 중 하나인 보강이나 사용자 정의 인터페이스로 단언를 사용해야 한다

설명

  1. 내장 타입에 데이터를 저장해야 하는 경우, 안전한 타입 접근법 중 하나인 보강이나 사용자 정의 인터페이스로 단언를 사용해야 한다

    //보강
    interface Document {
        monkey: string;
    }
    
    document.monkey = 'Tamarin';   //정상
    
    //사용자 정의 인터페이스로 단언
    interface MonkeyDocument extends Document {
        monkey: string;
    }
    
    (document as MonkeyDocument).monkey = 'Tamarin';  //정상

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

요약

  • noImplicitAny로 암묵적인 any타입 사용을 금지 시켜도, 명식적 any 또는 서드파티 타입 선언(@types)을 통해 any 타입은 코드 내에 여전히 존재할 수 있다는 점을 주의하자.
  • 작성한 프로그램 타입을 추적해 any 사용을 줄여나가자. npx type-coverage 를 사용해 프로젝트 심벌중 any 가 아닌 타입의 퍼센트를 확인할 수 있다. npx type-coverage --detail 를 사용해 any 타입이 있는 곳을 모두 출력할 수 있다.

디지엠유닛원 주식회사

  • 대표이사 권혁태
  • 개인정보보호책임자 정경영
  • 사업자등록번호 252-86-01619
  • 주소
    서울특별시 금천구 가산디지털1로 83, 6층 601호(가산동, 파트너스타워)
  • 이메일 web@dgmit.com