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

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

2022.09.10 #7

Closed moonheekim0118 closed 2 years ago

moonheekim0118 commented 2 years ago

오늘 타입스크립트 스터디 필수 참고 자료 ‼️

우리 회원님들 행복한 추석되세요 🌕

soyi47 commented 2 years ago

아이템 25. 비동기 코드에는 콜백 대신 async 함수 사용하기

아이템 26. 타입 추론에 문맥이 어떻게 사용되는지 이해하기

아이템 27. 함수형 기법과 라이브러리로 타입 흐름 유지하기

아이템 28. 유효한 상태만 표현하는 타입을 지향하기

효과적으로 타입을 설계하려면, 유효한 상태만 표현할 수 있는 타입을 만드는 것이 중요하다.

interface State {
    pageText: string;
    isLoading: boolean;
    error?: string;
}

function renderPage(state: State) {
    if (state.error)  {
        return `Error! Unable to load ${currentPage}: ${state.error}`;
    }
    else if (state.isLoading) {
        return `Loading ${currentPage}...`;
    }
    return `<h1>${currentPage}</h1>\n${state.pageText}`;
}

위는 애플리케이션의 상태를 나타내는 코드다. 여기에는 필요한 정보가 부족하다는 문제가 있다. 에러 상태와 로딩 상태를 정확히 구분하기 어렵고, 두 속성이 충돌할 수 있다. isLoading이 true이면서 error 값이 존재하면 로딩 중인지 오류 발생 상태인지 명확히 구분할 수 없다. 이는 정확히 구분이 어려운 무효한 상태다. 유효한 상태와 무효한 상태를 모두 표현하는 타입은 혼란을 초래하고 오류를 유발하기 쉽다.

다음과 같이 무효한 상태를 허용하지 않도록 제대로 표현할 수 있다. 현재 페이지가 무엇인지 명확하고, 모든 요청은 하나의 상태만 가진다.

interface RequestPending {
    state: 'pending';
}

interface RequestError {
    state: 'error';
    error: string;
}

interface RequestSuccess {
    state: 'ok';
    pageText: string;
}

type RequestState = RequestPending | RequestError | RequestSuccess;

interface State {
    currentPage: string;
    requests: { [page: string]: RequestState };
}

function renderPage(state: State) {
    const { currentPage } = state;
    const requestState = state.requests[currentPage];
    switch (requestState.state) {
        case 'pending':
            return `Loading ${currentPage}...`;
        case 'error':
            return `Error! Unable to load ${currentPage}: ${requestState.error}`;
        case 'ok':
            return `<h1>${currentPage}</h1>\n${state.pageText}`;
    }
}

아이템 29. 사용할 때는 너그럽게, 생성할 때는 엄격하게

interface LngLat { lng: number; lat: number; }; // 기본 형태
type LngLatLike = LngLat | { lon: number; lat: number; } | [number, number]; // 느슨한 형태

// 기본 형태
interface Camera {
    center: LngLat;
    zoom: number;
    bearing: number;
    pitch: number;
}

// 느슨한 형태
interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
    center?: LngLatLike;
}

type LngLatBounds = 
    {northeast: LngLatLike, southwest: LngLatLike} |
    [LngLatLike, LngLatLike] |
    [number, number, number, number];

declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): Camera;

아이템 30. 문서에 타입 정보를 쓰지 않기

liswktjs commented 2 years ago

한꺼번에 객체 생성하기

interface Point { x : number ; y : number ;}
const pt : Point = {}; -> 에러 발생! x, y가 존재하지 않는다 
const pt = { } as Point; -> 타입 단언을 사용하게 되면, 타입 체커문에서 통과할 수 있다

pt.x = 4;
pt.y = 2; 

일관성 있는 별칭 사용하기

비동기 토드에는 콜백 대신 async 함수 사용하기

prefer2 commented 2 years ago

Follow me, Come and get illusion~ 🎵 카리나는 신이에요 😭

타입 추론에 문맥이 어떻게 사용되는지 이해하기

타입스크립트는 타입을 추론할 때 값이 존재하는 곳의 문맥까지 살핀다.

type Language = 'JS' | 'TS' | 'Python';
function setLanguage(language: Language) { console.log(language) }

setLanguage('JS') // OK

let language = 'JS';
setLanguage(language) // Error
// Argument of type 'string' is not assignable to parameter of type 'Language'.

값을 변수로 분리해 내면, 타입스크립트는 할당 시점에 타입을 추론한다. language는 string으로 추론되었고, 이는 Language 타입에 할당이 불가능해 오류가 발생한다

해결 방법

튜플 사용시 주의점

function panTo(where: [number, number]) { /** */ }

panTo([10, 20]) // OK

const loc = [10, 20];
panTo(loc) // Error
// Argument of type 'number[]' is not assignable to parameter of type '[number, number]'.
// Target requires 2 element(s) but source may have fewer.

타입스크립트는 loc를 number[]로 추론한다. 배열의 길이를 알 수 없기 때문에 튜플 타입에 할당할 수 없다.

해결 방법

객체 사용 시 주의점

객체의 경우에도 튜플과 유사하다. 타입 선언을 추가하거나, 상수 단언(as const)를 사용하여 해결하자

콜백 사용 시 주의점

콜백을 상수로 뽑아내면 문맥이 소실된다.

function withCallback (fn: (n1: number, n2: number) => void) { fn(1,3) }

withCallback((a, b) => { console.log(a+b) }) // a: number, b: number로 추론된다
function withCallback (fn: (n1: number, n2: number) => void) { fn(1,3) }

const fn = (a, b) => { console.log(a+b) } // Parameter 'a' implicitly has an 'any' type.
withCallback(fn)

해결 방법

유효한 상태만 표현하는 타입을 지향하기

유효한 상태와 무효한 상태를 모두 표한하는 타입은 혼란스럽고 오류를 유발한다.

유효한 상태만 표현하는 타입을 지향하자

사용할 때는 너그럽게, 생성할 때는 엄격하게

함수의 매개변수는 타입의 범위가 넓어도 되지만, 결과를 반환할 때는 일반적으로 타입의 범위가 구체적이어야 한다. 매개변수 타입의 범위가 넓으면 사용하기 편리하지만, 반환 타입의 범위가 넓으면 불편하다. 사용하기 편리한 API일수록 반환 타입이 엄격하다.

문서에 타입 정보를 쓰지 않기

코드와 주석의 정보가 맞지 않을 수 있다. 타입스크립트의 타입 구문 시스템은 간결하고, 구체적이고, 쉽게 읽을 수 있도록 설계되었기 때문에 코드로 표현하는 것이 주석보다 더 나은 방법이다.

타입 주변에 null 값 배치하기

값이 null이거나 전부 null이 아닌 경우로 구분된다면 값을 다루기 쉬워진다. 타입에 null을 추가하는 방식으로 이러한 경우를 모델링 할 수 있다.

strictNullChecks 설정을 켜 null이나 undefined 관련 오류들을 확인해보자.

function extend(nums: number[]){
  let min, max;
  for (const num of nums){
    if(!min) {
      min = num;
      max = num;
    } else {
      min = Math.min(min, num);
      max = Math.max(max, num);
      // Argument of type 'number | undefined' is not assignable to parameter of type 'number'.
      // Type 'undefined' is not assignable to type 'number'.
    }
  }
   return [min, max];
}

min이 undefined인 경우만 제외했고, max에대해서는 제외하지 않았다. max에 대한 체크를 추가할 수도 있겠지만 더 좋은 방법을 찾아보자

function extend(nums: number[]){
  let result: [number, number] | null = null;
  for (const num of nums){
    if(!result) {
      result = [num, num];
    } else {
      result = [Math.min(num, result[0]), Math.max(num, result[1])];
    }
  }
   return result;
}

반환 타입이 [number, number] | null이라 사용하기 좋아졌다. null이 아님 단언(!)을 사용하면 min과 max값을 얻을 수 있다. 혹은 단순하게 if문으로 확인하여 값을 얻을 수도 있다.

class UserPosts {
  user: UserInfo | null;
  post: Post[] | null;

  constructor() {
    this.user = null;
    this.post = null;
  }

  async init(userId: string) {
    return Promise.all([
      async () => this.user = await fetchUser(userId);
      async () => this.post = await fetchPostsForUser(userId);
    ]);
  }

  getUserName() {
    // user, post가 null이거나 아니거나 다양한 경우가 존재
  }
}

클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 하는 것이 좋다.

속성값의 불확실성은 모든 메서드에 나쁜 영향을 미친다. null 체크가 난무하도록 하는 것이 아니라 설계를 바꾸어 null인 경우와 null이 아닌 경우의 상태를 다뤄야 한다.

class UserPosts {
  user: UserInfo;
  post: Post[];

  constructor(user: UserInfo, posts: Post[]) {
    this.user = user;
    this.post = posts;
  }

  static async init(userId: string): Promise<UserPosts> {
    cosnt [user, posts] = await Promise.all([
      fetchUser(userId);
      fetchPostsForUser(userId);
    ]);
    return new UserPosts(user, posts)
  }

  getUserName() {
    //
  }
}

null인 경우가 필요한 속성은 Promise로 바꾸면 안된다. 코드가 복잡해지며 모든 메서드가 비동기로 바뀌어야 한다.

moonheekim0118 commented 2 years ago

유효한 상태만 표현하는 타입을 지향하기

interface State {
  pageText: string;
  isLoading: boolean;
  error?:string;
}

위와 같은 타입이 있다면..

interface RequestPending{
  state: 'pending';
}

interface RequestError {
  state: 'error';
  error: string;
}

interface RequestSuccess{
  state: 'ok';
  pageText: string;
}

type RequestState = RequestPending | RequestError | RequestSuccess;

interface State {
  currentPage: string;
  requests: {[page:string] : RequestState}
}

사용할 때는 너그럽게, 생성할 때는 엄격하게

문서에 타입 정보를 쓰지 않기

타입 주변에 null 값 배치하기

function extent(nums:number[]){
  let min,max;
  for(const num of nums){
    if(!min){
      min = num;
      max = num;
    } else {
      min = Math.min(min,num);
      max = Math.max(max,num);
// 'number' | 'undefined' 형식의 인수는 'number' 형식의 매개변수에 할당 될 수 없습니다.
    }
  }
  return [min,max];
}
function extent(nums:number[]){
  let result: [number,number] | null = null;
  for(const num of nums){
    if(!result){
      result = [num,num];
    } else {
      result = [ Math.min(result[0],num), Math.max(result[1],num) ]

    }
  }
  return result;
}

유니온의 인터페이스 보다는 인터페이스의 유니온 사용하기

interface Layer {
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint |PointPaint;
}
interface FillLayer {
  layout: FillLayout;
  paint: FillPaint;
}

interface LineLayer {
  layout: LineLayout;
  paint: LinePaint;
}

interface PointLayer {
  layout: PointLayout;
  paint: PointPaint;
}

type Layer = FillLayer | LineLayer | PointLayer;
interface FillLayer {
   type :  'fill';
  layout: FillLayout;
  paint: FillPaint;
}

interface LineLayer {
  type :  'line';
  layout: LineLayout;
  paint: LinePaint;
}

interface PointLayer {
  type :  'point';
  layout: PointLayout;
  paint: PointPaint;
}

type Layer = FillLayer | LineLayer | PointLayer;

예시 2


interface Person {
  name: string;
  placeOfBirth?: string;
  dateOfBirth?: number;
}

interface Person { name: string; birth ?: { place: string; date: number; } }

- 위처럼 두개의 속성을 하나의 객체로 모으자.
- null 값을 경계로 두는것과 비슷하구만!

# string 타입보다 더 구체적인 타입 사용하기
1. 변수의 범위를 보다 정확하게 표현하고 싶다면 string 타입 보다는 문자열 리터럴 타입의 유니온 타입을 사용합시다.
2. 객체 속성 이름을 함수 매개변수로 받을 때는 string 보다 keyof T 를 사용합시다.

```ts
function pluck(records: any[], key:string): any[]{
  return records.map(r=>r[key]);
}

타입시그니처 개선 1 번

function pluck<T>(records: T[], key:string): any[]{
  return records.map(r=>r[key]);
}

개선 3번

function pluck<T>(records: T[], key: keyof T): T[keyof T] []{
  return records.map(r=>r[key]);
}

개선 2번

function pluck<T, K extends keyof T>(records: T[], key: K): T[K] []{
  return records.map(r=>r[key]);
}