woowacourse-study / 2022-thanks-giving-effective-typescript

🍂추석 연휴 집중🍂 이펙티브 타입스크립트를 읽는 모임 (✌️완주완료✌️)
6 stars 0 forks source link

2022.09.12 #9

Closed prefer2 closed 1 year ago

prefer2 commented 1 year ago

image

스폰지밥 마인드로 살아갑시다. 월요일 조아~🎵 https://www.youtube.com/watch?v=kLq3R2zCte8

prefer2 commented 1 year ago

타입스크립트는 정적이면서도 동적인 특성을 동시에 가진다. 따라서 타입스크립트는 프로그램의 일부분에만 타입 시스템을 적용할 수 있다. 마이그레이션을 할 때 코드의 일부분에 타입 체크를 비활성화시켜 주는 any 타입이 중요한 역할을 한다

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

function processBar(b: Bar) {}

function f() {
  const x = returningFoo();
  processbar(x); // Foo 형식의 인수는 Bar 형식의 매개변수에 할당할 수 없다
}

// solution 1
function f1() {
  const x: any = returningFoo();
  processbar(x);
}

// solution 2. 권장되는 방법
function f1() {
  const x = returningFoo();
  processbar(x as any);
}

두번째 방법인 x as any 형태가 권장된다. any타입이 함수의 매개변수에서만 사용되어 다른 코드에는 영향을 미치지 않기 때문이다. 1번 방법의 경우 호출 이후에도 x가 any타입인 반면, 2번째 방법은 호출 이후에는 x가 그대로 Foo 타입이다.

함수에서 any 타입을 반환하면 그 영향력이 프로젝트 전반으로 퍼지게 된다.

타입스크립트가 함수의 반환 타입을 추론할 수 있는 경우에는 함수의 반환타입을 명시하는 것이 좋다. 반환 타입을 명시하면 any 타입이 함수 바깥으로 영향을 미치는 것을 방지할 수 있다.

function f() {
  const x = returningFoo();
  // @ts-ignore
  processbar(x); 
}

@ts-ignore를 사용하면 다음 줄의 오류가 무시된다. 하지만 타입 체커가 알려주는 오류는 문제가 될 가능성이 높은 부분들이기 때문에 원인을 찾아 대처하는 것이 옳다.

// 객체 안의 한 개의 속성이 타입 오류를 가지는 경우
const config: Config = {
  a = 1,
  b = 2,
  c ={
    key: value // 오류
  }
}

// solution 1
const config: Config = {
  a = 1,
  b = 2,
  c ={
    key: value
  }
} as any
// 객체 전체를 any로 단언하면 다른 속성들(a,b)이 타입 체크가 되지 않는 부작용이 생긴다.

// solution 2
const config: Config = {
  a = 1,
  b = 2,
  c ={
    key: value as any
  }
}
// 최소한의 범위에서만 any를 사용하자

any를 구체적으로 변형해서 사용하기

any는 모든 값을 표현할 수 있는 매우 큰 범위의 타입이다. 일반적인 상황에서는 any보다 더 구체적으로 표현할 수 있는 타입이 존재할 가능성이 높기 때문에 더 구체적인 타입을 찾아 타입 안전성을 높여야 한다.

function getLengthBad(array: any){
  return array.length; // any
}

function getLength(array: any[]){
  return array.length; // number
}

any보다 any[ ]가 더 좋은 이유

함수의 매개변수를 구체화 할 때 배열의 배열 형태라면 any[ ][ ]처럼 선언하면 된다.

함수의 매개변수가 객체이긴 하지만 값을 알 수 없다면 {[key: string]: any}처럼 선언하면 된다. object 타입을 사용 할 수도 있다. object 타입은 객체의 키를 열거할 수는 있지만, 속성에 접근할 수는 없다(obj[key] 불가능)

객체이지만 속성에 접근할 수 없어야 한다면 unknown타입이 좋을 수도 있다.

함수 안으로 타입 단언문 감추기

함수의 내부로직이 복잡해 모든 부분을 타입으로 구현하는 것이 어렵다면 함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의를 정확하게 명시하는 것이 좋다.

declare function shallowEqual(a: any, b: any): boolean;

function shallowEqual<T extends object>(a: T, b: T): boolean {
  for( const [k, aVal] of Object.entries(a)) {
    if(!(k in b) || aVal !== b[k]){ // {} 형식에 인덱스 시그니처가 없어 암시적으로 any 형식이 있다
      return false;
    }
  }

  return Object.keys(a).length === Object.keys(b).length
}

// any를 사용해서 오류 해결. 
// 실제 오류가 아니라는 것을 알고 있기 때문에 any로 단언 가능
function shallowEqual<T extends object>(a: T, b: T): boolean {
  for( const [k, aVal] of Object.entries(a)) {
    if(!(k in b) || aVal !== (b as any)[k]){ 
      return false;
    }
  }

  return Object.keys(a).length === Object.keys(b).length
}

any의 진화를 이해하기

타입스크립트에서 일반적으로 변수의 타입은 변수를 선언할 때 결정되고, 이는 대부분 확장할 수 없다. 하지만 any타입은 예외인 경우가 있다.

function range(start: number, limit: number){
  const out = [];
  for( let i = start; i < limit; i++ ){
    out.push(i);
  }
  return out; // 반환 타입이 number[]로 추론됨
}

out의 타입은 any[ ]로 선언되었지만 number 타입의 값을 넣는 순간부터 number[ ]로 진화(evolve)한다. 배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되며 진화한다.

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

any타입의 진화는 noImplicitAny가 설정된 상태에서 변수의 타입이 암시적으로 any인 경우에만 일어난다. 명시적으로 any를 선언하면 타입이 그대로 유지된다.

타입의 진화는 값을 할당하거나 요소를 넣은 후에만 일어나기 때문에 편집기에서는 예상과 다르게 보일 수 있다.

암시적 any 샅애인 변수에 어떠한 할당도 하지 않고 사용하려고 하면 암시적 any 오류가 발생하게 된다.

function range(start: number, limit: number){
  const out = [];
  if( start === limit) {
    return out; // out 변수는 암시적으로 any[] 형식이 포함됩니다.
  }
  for( let i = start; i < limit; i++ ){
    out.push(i);
  }
  return out; 
}

any 타입의 진화는 암시적 any 타입에 어떤 값을 할당할 때만 발생한다. 그리고 어떤 변수가 암시적 any 상태일 때 값을 읽으려고 하면 오류가 발생한다.

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

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

any

unknown

let variable: unknown

variable = true // OK (boolean)
variable = 1 // OK (number)
variable = 'string' // OK (string)
variable = {} // OK (object)

let variable: unknown

let anyType: any = variable
let booleanType: boolean = variable
// Error: Type 'unknown' is not assignable to type 'boolean'.
let numberType: number = variable
//  Error: Type 'unknown' is not assignable to type 'number'.
let stringType: string = variable
//  Error: Type 'unknown' is not assignable to type 'string'.
let objectType: object = variable
//  Error: Type 'unknown' is not assignable to type 'object'.
function safeParse<T> (s: string): T {
  return parsed(s)
}

unknown 대신 제너릭 매개변수를 사용하는 경우도 있다. 하지만 이는 사실상 타입 단언문과 동일하다. 제너릭보다는 unknown을 반환하고 사용자가 직접 단언문을 사용하거나 원하는 대로 타입을 좁히도록 강제하는 것이 좋다.

soyi47 commented 1 year ago

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

function processBar(b: Bar) { /* */}
function expressionReturningFoo(): Foo { /* */ };

function f() {
    const x = expressionReturningFoo();
    processBar(x);  
        // 오류! 'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없다.
}

function f1() {
    const x: any = expressionReturningFoo();    
        // f1 범위에서는 x의 타입이 any다. 영향 범위가 크다.
    processBar(x);

        // 여기서 x를 반환까지 하면? any는 함수 바깥까지 영향을 미치게 된다.
}

function f2() {
    const x = expressionReturningFoo();
    processBar(x as any);       
        // 매개변수로 들어갈 때만 any다. 이후에 x는 다시 Foo 타입이다.
}

// any를 사용하지 않고 오류를 제거할 수도 있다.
function f3() {
    const x = expressionReturningFoo();
        // @ts-ignore
    processBar(x);       
}
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를 사용하여 다른 속성들은 타입 체크가 되도록!

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

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

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

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

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

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

moonheekim0118 commented 1 year ago

any 를 구체적으로 변형해서 사용하기

{[key: string]: any} 와 object 타입의 차이점

함수 안으로 타입 단언문 감추기

any 의 진화를 이해하기

any 진화되는 경우

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

any 타입

unknown 타입

any, unknown, never 차이

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


document.monkey = 'Hey';
// Error 

// 좋지 못한 해결방법1 any 단언문 사용하기

(document as any).monkey ='Hey';
  1. interface 특수 기능인 '보강' 사용하기

    interface Document{
    monkey: string;
    }
    • 타입이 더 안전하다.
    • 몽키패치가 어떤 부분에 적용되었는지 정확한 기록이 남는다.
    • 속성에 자동완성을 사용 할 수 있다.
    • 모듈 관점에서 제대로 동작하게 하려면 global 선언을 추가해야한다.
      declare global {
      interface Document{
      monkey: string;
      }
      }
    • 하지만 위와 같이 보강을 전역적으로 적용하면 코드의 다른 부분이나 라이브러리로부터 분리할 수 없다.
    • 그리고 애플리케이션이 실행되는 동안 속성을 할당하면 실행 시점에서 보강을 적용할 방법이 없다 ㅠㅠ
    • 특히 웹 페이지 내 HTML 엘리먼트를 조작할 때, 어떤 엘리먼트는 속성이 있고, 어떤 엘리먼트는 속성이 없는 문제가 있다.
  2. 더 구체적인 타입 단언문 사용하기

interface MonkeyDocument extends Document{ monkey:string; }

(document as MonkeyDocument).monkey='hey';


# devDependencies 에 typescript 와 @types 추가하기
### dependenies
- 현재 프로젝트를 실행하는데 필수적인 라이브러리들

### devDependencies
- 현재 프로젝트를 개발하고 테스트하는 데 사용되지만, 런타임에는 필요 없는 라이브러리들

### peerDependencies
- 런타임에 필요하긴 하지만, 의존성을 직접 관리하지 않는 라이브러리들
- 대표적인 예로 플로그인. 제이쿼리의 플러그인은 다양한 버전의 제이쿼리와 호환되므로 제이쿼리의 버전을 플러그인에서 직접 선택하지 않고 플러그인이 사용되는 실제 프로젝트에서 선택하도록 만들때 사용한다.

### 타입스크립트를 시스템 레벨로 설치하지 않는 이유
- 팀원들 모두가 항상 동일한 버전을 설치한다는 보장이 없다.
- 프로젝트를 셋업 할 때 별도의 단계가 추가된다.

# 타입 선언과 관련된 세 가지 버전 이해하기
세가지 버전
1. 라이브러리의 버전
2. 타입 선언(@types)의 버전
3. 타입스크립트의 버전

# TsDocs 사용하기
- 타입 정의에 주석처럼 사용하기!
- 너무 장황하게 쓰지 말기 
- 익스포트 된 함수, 클래스, 타입에 주석을 달 때는 TSDocs 형태를 사용하기
- 주석에 타입 정보 포함하지 않기

# 콜백에서 this 에 대한 타입 제공하기
- 자바스크립트에서 let 이나 const 로 선언된 변수는 렉시컬 스코프이지만, this 는 동적 스코프이다.
- 즉, 동적 스코프의 값은 '정의된 방식'이 아니라 '호출된 방식'에 따라 달라진다!

```ts
class C {
  vals = [1,2,3];
  logSquares(){
    for(const val of this.vals){
      console.log(vals * vals);
    }
  }
}

const c = new C();
const method = c.logSquares;
method(); // error

그리서 아래와 같이 명시적으로 this 를 바인딩 해주어야 한다.

class C {
  vals = [1,2,3];
  logSquares(){
    for(const val of this.vals){
      console.log(vals * vals);
    }
  }
}

const c = new C();
const method = c.logSquares;
method.call(c);  // 바인딩 ㅎㅎ

콜백 함수에서 this 를 사용해야 한다면 타입 명시해주기


function addKeyListener{
  el: HTMLElement,
    fn: (this: HTMLElement, e: (KeyboardEvent)=>void )
}

오버로딩 타입 보다는 조건부 타입을 사용하기.

function double(x: number | string): number | string;
function double(x:any){
    return x+x;
}

function double<T extends number|string>(x:T):T; function double(x:any){ return x+x; }

const num = double(12); // 타입이 12 const str = double('x'); // 타입이 "x"

- 타입이 너무 과하게 구체적이다.
- 그러면 여러가지 타입 선언으로 분리해보쟈!!

```ts
function double(x:number):number;
function double(x:string):string;
function double(x:any){
    return x+x;
}

const num = double(12); // 타입이 number
const str = double('x'); // 타입이 string
function double(x:number):number;
function double(x:string):string;
function double(x:any){
    return x+x;
}

const num = double(12); // 타입이 12
const str = double('x'); // 타입이 "x"

function f(x:number|string){
    return double(x); // Error 'string' | 'number' 형식의 인수는
                      // 'string' 형식의 매개변수에 할당될 수 없습니다!!
}

조건부 타입 사용하기


function double<T extends number | string>(x:T):T extends string ? string : number;
function double(x:any){
    return x+x;
}