본문 바로가기
패스트캠퍼스/자습

이펙티브 타입스크립트 정리 - 2장-02

by sunnykim91 2023. 5. 19.

매개변수나 반환값에 타입을 명시하기보다는 함수 표현식 전체에 타입 구문을 적용 하는 것이 좋다.

declare function fetch(
  input: RequestInfo,
  init?: RequestInit
): Promise<Response>;

async function checkdFetch(input: RequestInfo, init?: RequestInit) {
  const response = await fetch(input, init);
  if (!reponse.ok) {
    // 비동기 함수 내에서 거절된 프로미스로 변환함
    throw new Error("Request failed: " + response.status);
  }
  return response;
}

// 위코드도 잘 동작하겠지만, 아래 코드가 좀 더 좋다

const checkdFetch: typeof fetch = async (input, init) => {
  const response = await fetch(input, init);
  if (!response.ok) {
    // 비동기 함수 내에서 거절된 프로미스로 변환함
    throw new Error("Request failed: " + response.status);
  }
  return response;
};

// 함수표현식으로 바꿨고, 함수 전체에 타입적용으로 타입스크립트가 input과 init의 타입을 추론할수있게함

타입과 인터페이스의 차이

type TState = {
  name: string;
  capital: string;
};

interface IState {
  name: string;
  capital: string;
}

인덱스 시그니처, 함수 타입, 제네릭 모두 사용하는데 크게 차이가 없음

interface는 타입을 확장할 수 있고, 타입은 인터페이스를 확장 가능 하다.

interface IstateWithPop extends Tstate {
  population: number;
}
type TstateWithPop = IState & { population: number };

인터페이스는 유니온 타입 같은 복잡한 타입을 확장하지는 못한다. -> 타입을 사용하여야한다.

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

  • 유니온 타입은 있지만 유니온 인터페이스는 없다.
  • 인터페이스는 보강이 가능하다-선언병합이 가능(아래예제)
interface IState {
  name: string;
  capital: string;
}
interface IState {
  poplulation: number;
}
const wyoming: IState = {
  name: "ming",
  capital: "seoul",
  population: 500,
}; // 에러가 발생하지 않고 잘 동작한다.

복잡한 타입의 경우 타입으로 표현하는게 맞지만, 그게 아닐 경우 interface, type 둘 중에 고민해본다.

interface 들을 합성할 경우 이는 캐시가 되지만, 타입의 경우에는 그렇지 못하다.

타입 합성의 경우, 합성에 자체에 대한 유효성을 판단하기 전에, 모든 구성요소에 대한 타입을 체크하므로 컴파일 시에 상대적으로 성능이 좋지 않다.

타입의 중복 줄이기

function distance(a: {x:number, y:number}, b: {x:number, y:number}) {
    ...
}

interface Point2D{
    x:number
    y:number
}
function distance(a:Point2D, b:Point2D) {...}
function get(url:string, opts:Options) : Promise<Reseponse> {...}
function post(url:string, opts:Options) : Promise<Reseponse> {...}

type HTTPFunction = (url:string, opts:Options) => Promise<Response>;
const get: HTTPFuction = (url, opts) => {...}
const post: HTTPFuction = (url, opts) => {...}

중복을 제거하는 과정 및 Pick

interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}

interface TopNavState {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
}

// 위 코드를 아래 형태로

type TopNavState = {
  userId: State["userId"];
  pageTitle: State["pageTitle"];
  recentFiles: State["recentFiles"];
};

// 아래와 같은 형식으로 중복 제거

type TopNaveState = {
  [k in "userId" | "pageTitle" | "recentFiles"]: State[k];
};

// Pick이라는 제너릭타입을 활용 하여 중복 없애기

type TopNavState = Pick<State, "userId" | "pageTitle" | "recentFiles">;
interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}

interface OPtionsUpdate {
  width?: number;
  height?: number;
  color?: string;
  label?: string;
}

class UIWidget {
  constructor(init: Options) {}
  update(options: OPtionsupdate) {}
}

위코드에서 keyof를 사용하면 OptionsUpdate를 만들 수 있다.

type OptionsUpdate = { [k in keyof Options]?: Options[k] };

Partial의 활용

class UIWidget {
  constructor(init: Options) {}
  update(options: Partial<Options>) {}
}

제너릭 타입의 매개변수 제한할 수 있는 방법은 extends를 사용하는것
extends를 이요함녀 제너릭 매개변수가 특정 타입을 확장한다고 선언할 수 있다.

interface Name {
  first: string;
  last: string;
}

type DancingDuo<T extends Name> = [T, T];

const couple1: DancingDuo<Name> = [
  { first: "Fred", last: "bob" },
  { first: "Fred2", last: "bob2" },
];

const couple2: DancingDuo<{ first: string }> = [
  // {first:string} 에러 발생 ,  {first:string} 는 Name을 확장하지 않기 때문에 오류 발생
  { first: "sonny" },
  { first: "sonny2" },
];

//Type '{ first: string; }' does not satisfy the constraint 'Name'.
//Property 'last' is missing in type '{ first: string; }' but required in type 'Name'.(2344)

Partial 예제 (파셜 타입은 특정 타입의 부분 집합을 만족하는 타입을 정의할 수 있습니다)

interface Address {
  email: string;
  address: string;
}

type MyEmail = Partial<Address>;
const me: MyEmail = {}; // 가능
const you: MyEmail = { email: "noh5524@gmail.com" }; // 가능
const all: MyEmail = { email: "noh5524@gmail.com", address: "secho" }; // 가능

Pick (픽 타입은 특정 타입에서 몇 개의 속성을 선택하여 타입을 정의합니다)

interface Product {
  id: number;
  name: string;
  price: number;
  brand: string;
  stock: number;
}

// 상품 목록을 받아오기 위한 api
function fetchProduct(): Promise<Product[]> {
  // ... id, name, price, brand, stock 모두를 써야함
}

type shoppingItem = Pick<Product, "id" | "name" | "price">;

// 상품의 상세정보 (Product의 일부 속성만 가져온다)
function displayProductDetail(shoppingItem: shoppingItem) {
  // id, name, price의 일부만 사용 or 별도의 속성이 추가되는 경우가 있음
  // 인터페이스의 모양이 달라질 수 있음
}

Omit Pick의 반대

interface Product {
  id: number;
  name: string;
  price: number;
  brand: string;
  stock: number;
}

type shoppingItem = Omit<Product, "stock">;

const apple: Omit<Product, "stock"> = {
  id: 1,
  name: "red apple",
  price: 1000,
  brand: "del",
};

인덱스 시그니처란

type Rocket = { [property: string]: string }; // 이부분이 인덱스 시그니처
const rocket: Rocket = {
  name: "a",
  variant: "v2",
  thrust: "333",
};

하지만 문제점이 존재

  • name대신 Name이어도 유효한 타입이되버린다
  • 특정 키가 필요하지 않음 {}도 허용
  • 키마다 다른 타입을 가질 수 없다. thrust는 string이 아니라 number여야 할 수도있음
  • 타입스크립트 언어서비스가 동작하지 않는다.

인덱스 시그니처로 사용할바엔 interface를 사용한다.

Record란

//2개가 같은 의미
type Vec3D = Record<"x", "y", "z", number>;

type Vec3D = {
  x: number;
  y: number;
  z: number;
};

타입스크립트는 숫자 키를 허용하고, 문자열 키와 다른것으로 인식한다.

x = [1, 2, 3];
x[0]; // 1

x["1"]; // 결과값: 2 , 문자열로 접근해도 가능

Objects.keys(x); // ['0','1','2'] 키가 문자열로 출력됨

for..of 문은 인덱스에 신경쓰지 않을때
배열의 forEach는 인덱스의 타입이 주용할때
루프중간에 멈춰야한다면 그냥 for문

for-in은 다른 for문들보다 느리기때문에 지양하자.

튜플은 고정된 개수의 요소로 구성된 배열이다.(배열의 서브타입)

const myTuple = [1, "Hello", true];

위의 예제에서 myTuple은 세 개의 요소로 구성된 튜플입니다.
첫 번째 요소는 숫자, 두 번째 요소는 문자열, 세 번째 요소는 boolean 값입니다.
튜플의 요소는 인덱스를 사용하여 접근할 수 있습니다.
예를 들어, myTuple[0]은 1을 반환하고 myTuple[1]은 "Hello"를 반환합니다.

readonly

readonly number[]는 number[] 서브타입이 된다.

매개변수를 readonly로 할경우

  • 타입스크립트는 매개변수가 함수 내에서 변경이 일어나는지 체크
  • 호출하는 쪽에서는 함수가 배개변수를 변경하지 않는다는 보장을 받게됨
  • 호출하는 쪽에서 함수에 readonly배열을 매개변수로 넣을 수도있다.

매개변수를 수정하지 않을거라면 readonly를 붙이면 인터페이스를 명확하게 할 수 있고, 변경할떄 에러를 발생시키기에 오류확인할때 좋다!

const 와 readonly의 차이

const는 변수 참조를 위한 것, 변수에 다른 값을 할당할 수 없다.

readonly는 속성을 위한 것

type readonlyA = {
  readonly barA: string;
};

const x: readonlyA = { barA: "baz" };
x.barA = "quux"; // ⛔️ 변경 불가
type readonlyB = {
  readonly barB: { baz: string };
};

const y: readonlyB = { barB: { baz: "quux" } };
y.barB.baz = "zebranky"; // 👌 변경 가능

이예제가 가능한 이유는 readonly가 얕게 동작하기 때문
->barB가 참조하고 있는 값 자체는 변경될 수 없지만
얘가 readonly라고 그 안에 있는 속성들이 모두 동일한 접근 제어자를 가지고 있는 것이 아니다.

데이터나 디스플레이 속성이 변경되면 다시 그려야하지만, 이벤트 핸들러가 변경되면 다시 그릴 필요가 없다는 것에 대한 예제

interface ScatterProps {
    xs: number[];
    ys:number[];

    xRange: [number,number];
    yRange: [number,number];
    color:string;

    onClick: (x:number, y:number, index:number) => void;
}


const REQUIRES_UPDATE: {[k in keyof ScatterProps]:boolean} = {  // 타입체커에게 REQUIRES_UPDATE가 ScatterProps와 동일한 속성을 가져야한다는것을 제공
    xs: true,
    ys: tr
    xRange: true,
    yRange: true,
    color:true,
    onClick:false
}

function shouldUpdate(oldProps:ScatterProps, newProps:ScatterProps) {
    let k: keyof ScatterProps;
    for(k in oldProps) {
        if(oldProps[k] !== newProps[k] && REQUIRES_UPDATE[k]) {
            return true;
        }
    }
    return false
}

매핑된 타입을 사용하여 관련된 값과 타입을 동기화할것!
어떤 새로운 속성을 추가할때에도 선택을 강제할 수 있도록(오류를 줄일 수 있도록) 매핑된 타입을 사용하자

매핑타입 추가 정보

TypeScript의 매핑 타입은 기존 타입을 변형하여 새로운 타입을 생성하는 기능을 제공합니다. 매핑 타입은 제네릭 타입과 조건부 타입을 이용하여 구현됩니다.

가장 일반적인 매핑 타입은 Record 타입입니다. Record<K, V>는 키 타입 K와 값 타입 V를 매핑하여 새로운 객체 타입을 생성합니다.

예를 들어, Record<string, number>는 문자열 키를 가지고 숫자 값을 가지는 객체를 나타냅니다.

또 다른 유용한 매핑 타입은 Partial과 Readonly입니다.
Partial는 타입 T의 모든 속성을 선택적으로 만들어줍니다. Readonly는 타입 T의 모든 속성을 읽기 전용으로 만들어줍니다.

매핑 타입은 keyof 연산자와 함께 사용하여 타입의 특정 속성을 추출하거나 조작할 수도 있습니다.
예를 들어, keyof T는 타입 T의 모든 속성 키를 추출합니다.(위 예제)

반응형