Effective Typescript (5)

Jaewoong2·2022년 12월 29일
0

타입 설계

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

  • 예를 들어, role: "user" 일때, expireTime: 600000 이고, role: "admin" 일때는 expireTime 이 없다고 했을때 Type을 아래와 같이 설정 할 수 있다.
type User = {
  role: "user" | "admin";
  expireTime?: number;
}
  • 이 경우, role이 user 일때 expireTime이 있는지, role이 admin일때 expireTime이 있는지 알 방법이 없다.

  • 이를 명확하게 하기 위해, role의 값에 따라, expireTime 을 명시 해주는 타입을 각각 만들어주는 타입을 만들어줘야한다

type User = {
  role: "user";
  expireTime: number;
}

type Admin = {
  role: "admin";
}

type Character = User | Admin;
  • 태그된 유니온 기법을 사용 하는 것인데, 이러면 Character 타입의 값들은 role 에 따른 유효한 타입 체크를 할 수 있다.

  • 아래는 네트워크 요청에 따른 예시 이다.

interface RequestPending {
  state: 'pending';
}

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

interface RequestSuccess {
  state: "success";
  pageText: string;
}

type RequestState = RequestPending | RequestError | RequestSuccess

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

// 태그된 유니온 기법 (네트워크 요청 과정 각각의 상태를 명시적으로 모델링)
const render = (state: State) => {
  const { currentPage } = state;
  const requestState = state.requests[currentPage];
  
  // state: "pending" | "error" | "success" 으로 자동 추론
  if (requestState.state === 'pending') {
    // const requestState: RequestPending
    return `Loaindg... ${currentPage}`
  }
  
  if (requestState.state === 'error') {
    // const requestState: RequestError
    return `Error in ${currentPage}, ${requestState.error}`
  }

  if (requestState.state === 'success') {
    // const requestState: RequestSuccess
    return `<h1>${currentPage}</h1> ${requestState.pageText}`
  }

  return `Unknown Erorr`
}

const changePage = async (state: State, page: string) => {
  state.requests[page] = { state: "pending" };
  state.currentPage = page;
  
  try {
    const res = await fetch(page);
    if (!res.ok) {
      throw new Error("Error in " + state.currentPage + ":" +res.statusText);
    }
    const pageText = await res.text();
    state.requests[page] = { state: "success", pageText };

  } catch (err) {
    state.requests[page] = { state: "error", error: `${err}` };
  }

}

사용하기는 쉽게, 생성 할 때는 어렵게

  • 라이브러리를 만들 때, 사용자에게 다양한 매개변수를 사용 할 수 있도록 Optional 로 설정 하거나, 다양한 타입을 union 타입으로 지정 해주게 되는 경우가 많다.

  • 하지만, 반환 타입이 여러개가 된다면 사용자에 입장에서, 타입체커의 입장에서 함수의 반환 타입을 확실하게 하지 못해 제대로 사용 할수 없다.

  • 이를 위해 반환 할때는 명확하게 반환 할 수 있도록 설계를 해야 한다.

/** 사용하기는 쉽게, 생성할 때는 어렵게
 * - 매개변수의 타입은 넓게 들어오도록 
 * - 반환 타입은 되도록 하나의 타입을 반환 하도록 설계하자
 */

type LngLat = 
{
  lng: number;
  lat: number;
} | 
{
  lon: number;
  lat: number;
} | 
[number, number]

type CameraOptions = {
  center?: LngLat;
  zoom?: number;
  bearing?: number;
  pitch?: number;
}

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

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

function focustOnFeature() {
  const bounds: [LngLat, LngLat] = [{ lng: 3, lat: 4 }, [4, 5]];


  // const zoom: number | undefined
  // (property) center?: LngLat | undefined
  // Property 'lat' does not exist on type 'LngLat | undefined'.(2339) 
  // Property 'lng' does not exist on type 'LngLat | undefined'.(2339) 
  const { center: { lat, lng }, zoom } = viewportBounds(bounds);
}

- 반환 타입을 좁히지 못해서 나오는 오류
- 반환 타입을 구체적으로 작성 해주자
  • 함수의 사용자가 사용하기 편하게 다양한 타입의 변수를 매개변수로 사용 할 수 있도록 하였다.
    하지만, viewportBounds 의 반환 값 또한 optional 로, 타입이 구체적이지 못하다.

  • 이에 따라, 타입체커가 반환 값에 값이 실제로 있는지, undefined 인지 체크를 할 수 없기 때문에 에러가 발생 한다.

/** 사용하기는 쉽게, 생성할 때는 어렵게
 * - 매개변수의 타입은 넓게 들어오도록 
 * - 반환 타입은 되도록 하나의 타입을 반환 하도록 설계하자
 */

type LngLat = { lng: number; lat: number; }

type LngLatLike = LngLat | 
{
  lon: number;
  lat: number;
} | 
[number, number]

type Camera = {
  center: LngLat;
  zoom: number;
  bearing: number;
  pitch: number;
}

type CameraOptions = 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 viewportBounds(bounds: LngLatBounds): Camera;

function focustOnFeature() {
  const bounds: LngLatBounds = [{ lng: 3, lat: 4 }, [4, 5]];

  // (property) center: LngLat
  // const lat: number
  // const lng: number
  // const zoom: number
  const { center: { lat, lng }, zoom } = viewportBounds(bounds);
}
  • 위의 예시 처럼 매개 변수나, 사용하는 변수 들의 타입은 LngLatLike, CameraOptions 등의 타입으로 넓게 정해주고

  • 반환 값은 Camrea, LngLat 으로 구체적으로 좁혀주었다.

이렇게 함으로써, 반환 값을 사용할때 어떠한 오류가 발생하지 않는 것을 파악 할 수 있다.


타입에 null 값은 어떻게 넣을까

  • 어떠한 객체(object, array 등)의 속성 값이 null 이 될 수 있으면
  • 조금 더 크게 객체가 null 이 되던지, 객체의 속성이 모두 값이 있던지로 설계를 하는 것이 좋다
/**
 * 타입 주변에 null 값 배치하기
 * - 값이 전부 null 이 되도록 설계 하거나
 * - 값이 전부 null 아 이난 경우로 설계 해라
 * - 변수가 null이 되는 것을 사람이든 타입체커든 확인하기 어렵기 때문에
 */


function getMinMax(numbers: number[]) {
  // 값을 갖고 있던지, null 을 갖고 있던지 확실하게 해야함
  let result: [number, number] | null = null

  for (const num of numbers) {
    if (result === null) {
      result = [num, num]
    } else {
      result = [Math.min(num, result[0]), Math.max(num, result[1])]
    }
  }

  return result
}


/**
 * API 를 작성할 때에는, 반환 타입을 큰 객체로 만들고 확실하게 해줘야한다
 * - 반환 타입 전체가 null (O)
 * - 반환 타입 전체가 null (X)
 */

// const getUser: () => Promise<{
//     id: number;
//     name: string;
// } | null>

const getUser = async () => {
  const response = await fetch('/getUer');
  
  if (!response.ok) {
    return null
  }

  const data: {
    id: number;
    name: string;
  } = await response.json()


  return data
}

유니온의 인터페이스 < 인터페이스의 유니온

  • 여러개의 선택적 필드 (optional property) 가 동시에 값이 있거나 동시에 undefined 인 경우에는 태그된 유니온 패턴 사용하자
/**
 * - 유니온의 인터페이스 보다는 인터페이스의 유니온을 사용하기
 * - 여러개의 선택적 필드 (optional property) 가 동시에 값이 있거나 동시에 undefined 인 경우에는 `태그된 유니온 패턴` 사용하기
 */


interface A {
  status: "loading" | "success" | "error"
  title?: string
  errorMessage?: string
}

interface Loading {
  status: "loading";
}

interface Success {
  status: "suceess";
  title: string
}

interface Error {
  status: "error";
  errorMessage: string;
}

type B = Loading | Success | Error

// function findSomethingA(something: A): string | undefined
function findSomethingA(something: A) {
  if (something.status === 'error') {

    // something.errorMessage 의 값이 있는지 undefined 인지 모름
    // (property) A.errorMessage?: string | undefined
    return something.errorMessage
  }

  if (something.status === 'loading') {
    return `loading...`
  }

  // something.title 의 값이 있는지 undefined 인지 모름
  // (property) A.title?: string | undefined
  return something.title
}


// 타입에 따른 조건부 처리가 쉬움
// function findSomethingB(something: B): string
function findSomethingB(something: B) {
  if (something.status === 'error') {
    // (parameter) something: Error
    return something.errorMessage
  }

  if (something.status === 'loading') {
    // (parameter) something: Loading
    return `loading...`
  }

  // (parameter) something: Success
  return something.title
}
/**
 * - 유니온의 인터페이스 보다는 인터페이스의 유니온을 사용하기
 * - 여러개의 선택적 필드 (optional property) 가 동시에 값이 있거나 동시에 undefined 인 경우에는 `태그된 유니온 패턴` 사용하기
 */

interface Person {
  name: string;

  // 둘다 동시에 있거나 동시에 없다
  placeOfBirth?: string;
  dateofBirth?: Date;
}

interface Person2 {
  name: string;
  birth?: {
    place: string;
    date: string;
  }
}

// 이렇게 할 경우 birth 속성 하나만 체크 하면, birth.place 와 birth.date 의 유무를 타입 체크를 할 수 있다.
profile
DFF (Development For Fun)

0개의 댓글