Effective Typescript (3)

Jaewoong2·2022년 12월 27일
0

잉여속성 체크

  • 잉여속성 체크란, 변수에 타입을 선언함과 동시에 오브젝트 리터럴로 만들게 되면 잉여 속성을 체크 한다
interface Animal {
  bark: true;
  fur: true;
}

const dog: Animal = {
  bark: true,
  fur: true,
  wing: false
// ~~~~~~~~~~~ 객체 리터럴은 알려진 속석만 지정 할 수 있으며, "Animal" 형식에 
// (wing) 속성이 없습니다.
}

- 타입이 명시된 변수에 객체 리터럴을 할당 할 때, 
  1) 타입스크립트는 해당 타입의 속성이 있는지
  2) 그 외의 속성은 없는지 확인 한다.
  • 구조적 타이핑 관점으로 생각 해보면 오류가 발생하지 않아야 한다.
  • 임시 변수를 도입 해보면, 구조적 타이핑 관점으로 할당이 가능하다.
interface Animal {
  bark: true;
  fur: true;
}

const dog = {
  bark: true,
  fur: true,
  wing: false
}

const d: Animal = dog;

// dog 객체는 Animal의 부분집합을 포함하고 있기 때문에, 구조적 타이핑 관점으로 할당이 가능하다.

즉, 타입스크립트의 잉여타입 검사할당 가능 검사 는 다르다는 것을 알 수 있다.

  • 잉여타입검사의 이점으로는 bark 속성을 brak 으로 잘못 기입 하였을 때, 이를 검사 할 수 있다는 이점이 있다. ('엄격한 객체 리터럴 체크' 라고도 불림)

잉여타입 검사는 1) 타입지정과 함께 객체 변수를 선언 할때 2) 함수의 매개변수로 리터럴 객체를 넣을 때 이루어진다.

잉여타입 검사를 회피하기 위해서는 1) 임시변수를 사용해 객체를 할당 하는 것 2) 인덱스 시그니처를 사용하여 타입스크립트가 추가적인 속성을 예상 하도록 하기

interface Animal {
  bark: true;
  fur: true;
  [other: string]: unknown;
}

const dog: Animal = {
  bark: true,
  fur: true,
  wing: false
} // 정상

함수 표현식에 타입 적용하기

  • 타입스크립트에서는 함수 표현식을 사용하는 것이 좋음
    1) 함수의 매개변수 부터 반환값 까지 전체를 함수 타입으로 선언하여 함수 표현식에 재사용 할 수 있다는 장점이 있다.
// 함수 선언식
function addFunction = (a: number, b: number) => a + b;
function subFunction = (a: number, b: number) => a - b;
function mulFunction = (a: number, b: number) => a * b;
function divFunction = (a: number, b: number) => a / b;

// -------------

// 함수 표현식
type BinaryFunction = (a: number, b: number) => number;
const addFunction: BinaryFunction = (a, b) => a + b;
const subFunction: BinaryFunction = (a, b) => a - b;
const mulFunction: BinaryFunction = (a, b) => a * b;
const divFunction: BinaryFunction = (a, b) => a / b;

2) 함수의 매개변수에 타입을 선언 하는 것 보다, 함수 표현식 전체 타입을 정의 하는 것이 코드도 간결하고 안전하다. return 값에 대한 오류 까지 찾아줌

3) 다른 함수의 시그니처와 동일한 타입을 가지는 새 함수를 작성 하거나, 동일한 타입 시그니처를 가지는 여러개의 함수를 작성 할 때는 매개변수의 타입과 반환 타입을 반복해서 작성하지 말고 함수 전체의 타입 선언을 적용 해야한다.

async function checkStatusFetch(input: RequestInfo, init?: RequestInit) {
  const res = await fetch(input, init);
  if (!res.ok) {
    throw new Error("오류 발생" + res.status);
  }
  
  return res;
}


const checkStatusFetch: typeof fetch = (input, init) => {
  const res = await fetch(input, init);
  if (!res.ok) {
    throw new Error("오류 발생" + res.status);
    // return new Error(res.status); - fetch 함수의 반환 타입을 보장 하기 때문에 return 으로 오류를 반환 하면 타입 오류가 나온다.
  }
  
  return res;
}


인터페이스와 타입의 차이점

  1. Typeunion, intersection, 조건부 타입등 복잡한 타입 구조를 가진다.
  2. Interface는, 보강(augment)이 가능하다.
interface Person {
  name: string;
  age: number;
}

interface Person {
  gender: "F" | "M"; 
}

const Peter: Person = { 
  name: "Peter",
  age: 25,
  gender: "M"
} // 오류 없음

해당 예제 처럼 속성을 확장 하는 것을 선언 병합이라 한다.

그래서 Type / Interface 중 어떤 것을 사용 해야하는 가?

  • 복잡한 타입 구조가 예상 되는 경우 Type을 사용하는 것이 이점이 있음
  • 현재 프로젝트가 Type 이면 일관된 Type 을 사용 하기
  • 현재 프로젝트가 Interface 이면 일관된 Interface 을 사용 하기
  • 타입을 보강 하는것이 예상 되는 경우 Interface 를 사용 하는 것이 이점이 있음

타입 연산과 제너릭 사용으로 반복 줄이기

  • 타입 을 만들 때에도 DRY(Don't Repeat Yourself) 원칙을 지켜야한다.
// 수정 전
function getUser(url: string, options: Options): Promise<Response> { ... }
function postUser(url: string, options: Options): Promise<Response> { ... }

// 수정 후

type HttpFunction = (url: string, options: Options) => Promise<Response>;
                                                                     const getUser: HttpFunction = (url, options) => { ... }
const postUser: HttpFunction = (url, options) => { ... }
  • interface, extends | type, & 을 이용해서 반복을 제거 할 수 있다.
interface Person {
  name: string;
  family: string;
}

interface PersonWithAge extends Person {
  age: number;
}


type PersonWithMBTI = Person & { MBTI: string }
  • 인덱싱 타입으로 반복을 줄일 수 있다.
// 수정 전
interface HomeState {
  id: string;
  title: string;
  contents: string[];
}

interface NavProps {
  title: string;
  contents: string[];
}

/* 
HomeState의 부분 집합으로 Nav의 상태를 결정 해주는 것이 바람직 함
전체 어플리케이션의 상태를 하나의 인터페이스(타입) 으로 유지 할 수 있게 해준다
*/

// 수정 후
interface NavProps {
  title: HomeState["title"];
  contents: HomeState["contents"];
}

/* 
매핑된 타입을 이용 하면 코드의 양을 더 줄일 수 있다.
*/

interface NavProps {
 [k in "title" | "contents"]: HomeState[k]; 
}

/*
type Pick<T, K> = { [k in K]: T[k] };
*/

type NavProps = Pick<HomeState, "title" | "contents">;
  • Pick<T, K extends keyof T> = { [k in K]: T[k] };
    • K 는 인덱스로 사용 될 수 있는 string | number | symbol 이 되어야 한다
    • 이를 위해 K는 T의 키의 부분 집합을 받아올 수 있도록 K extends keyof T 를 사용
  • Partial<T> = { [k in keyof T]?: T[k] };
  • ReturnType<typeof Function>: 함수의 반환 값에 대한 타입

매핑된 타입 사용 하기

  • Type 에 속성을 추가 할 때, 속성 추가에 따른 함수를 고쳐야 하는 경우가 있다. 이를 해결 하기 위해서는 ("매핑된 타입")을 사용 하는 것이 좋다.

  • ("매핑된 타입")을 타입으로 하는 (객체 or key들의 배열)를 조건문에 넣어 타입스크립트가 타입 체크를 할 수 있도록 하여, Type 속성 추가 및 수정에 따른 함수 변경이 강제 될 수 있도록 해야 한다.

type Props = {
  age: number;
  gender: "F" | "M";
  name: string;
}

const CHECK_UPDATE: {[k in keyof Props]: boolean} = {
  age: true,
  gender: true,
  name: true,
}

function checkUpdate(oldProps: Props, newProps: Props) {
  let key: keyof Props;
  for (key in oldProps) {
    if (oldProps[key] !== newProps[key] && CHECK_UPDATE[k]) {
      return true;
    }
  }
  
  return false;
}
  • 이런 식의 코드 구성을 통해 Props 에 속성을 수정 하여도 타입 체크가 되도록 하는 것이 중요하다

타입 추론

  • 타입 추론을 적극적으로 사용 하는 것이 좋다
    • 타입 추론이 의도된 대로 추론이 잘 되면 명시적으로 작성해줄 필요가 없다
  • 명시적으로 타입을 지정 해주는 것
    • 객체 리터럴에 대한 정확한 Type을 제공 할 때
      • 타입 추론이 의도대로 추론이 되지 않을 수 있다.
    • 함수의 반환에도 타입을 명시해 오류를 방지 할 때
     
     interface Vector {
       x: number;
       y: number;
     }
     function add(a: Vector, b: Vector) { 
       return {x: a.x + b.x, y: a.y + b.y };
     }
     // 이 경우, return Type 은 { x: number, y: number } 로 추론이 된다.
     // 이를 해결 하기 위해서 함수에 대한 반환타입을 명시 해주는 것이 중요하다.
profile
DFF (Development For Fun)

0개의 댓글