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

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

by sunnykim91 2023. 6. 2.
let x: number = 12; // 이렇게 써도 되긴하지만

let x = 12; // 이렇게 써도 x는 number로 추론된다

const person: {
  name: string;
  born: {
    where: string;
    when: string;
  };
  died: {
    where: string;
    when: string;
  };
} = {
  name: "name",
  born: {
    where: "bornwher",
    when: "bornwhen",
  },
  died: {
    where: "diedwhere",
    when: "diedwhen",
  },
};

const person = {
  name: "name",
  born: {
    where: "bornwher",
    when: "bornwhen",
  },
  died: {
    where: "diedwhere",
    when: "diedwhen",
  },
}; // 객체 또한 동일하게 타입을 지정하지 않아도 추론된다

타입 구문을 제대로 명시할떄와 제거할때

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

function logProduct(product: Product) {
  const { id, name, price } = product;
  console.log(id, name, price);
}

// 타입 구문을 제거하였을때
const furby = {
  name: "a",
  id: 1234,
  price: 100,
};

logProduct(furby); // 에러발생, 객체를 선언한곳이아니라 사용되는곳에서 오류가 발생!

// Argument of type '{ name: string; id: number; price: number; }' is not assignable to parameter of type 'Product'.
//   Types of property 'id' are incompatible.
//     Type 'number' is not assignable to type 'string'

//타입 구문을 제대로 명시했을때 , 실수가 발생한 부분에 오류 발생
const furby: Product = {
  name: "a",
  id: 1234, // 에러발생 Type 'number' is not assignable to type 'string'.(2322)
  price: 100,
};

logProduct(furby);

함수도 반환타입 명시하기!

function getQuote(ticker: string) {
  return fetch(`https://quotes.example.com/?1=${ticker}`).then((response) =>
    response.json()
  );
}

const cache: { [ticker: string]: number } = {};
function getQuote(ticker: string): Promise<number> {
  // 함수 반환 타입을 명시하여야 오류의 표시위치가 정확하게 어디인지 알 수 있다.
  if (ticker in cache) {
    return cache[ticker]; // 해당부분에러 발생
  }

  return fetch(`https://quotes.example.com/?1=${ticker}`)
    .then((response) => response.json())
    .then((quote) => {
      cache[ticker] = quote;
      return quote;
    });
}

반환타입을 명시해야하는 이유

  • 함수에 대해 더욱 명확하게 할 수 있다.
  • 명명된 타입을 사용하기 위해서 이다.

서로 다른 타입에는 별도의 변수를 사용하자

let id = "12-34-56";
fetchProduct(id);
id = 123456; // 에러가 발생,  위에서 string으로 타입추론되어 number형식을 string형식에 할당할수없음!
fetchProdcutById(id);

이유

  • 서로 관련이 없는 두개의 타입을 분리
  • 변수명을 더 구체적으로 지을 수 있음
  • 타입추론을 향상시키며, 타입구문이 불필요해짐
  • 타입이 간결해짐
  • let대신에 const를 사용하게되어 타입체커가 추론하기 좋음

타입 넓히기 : 타입체커가 타입을 결정해야하는데, 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추해내는 과정

interface Vector3 {
  x: number;
  y: number;
  z: number;
}
function getComponent(vector: Vector3, axis: "x" | "y" | "z") {
  return vector[axis];
}

let x = "x";
let vec = { x: 10, y: 20, z: 30 };

getComponent(vec, x); //에러발생,  Argument of type 'string' is not assignable to parameter of type '"x" | "y" | "z"'

위에서 선언한 let x 의 타입이 string 으로 추론되어서 에러발생

const mixed = ["x", 1]; // 이 코드 하나도 타입스크립트는 다양한 타입으로 추론이 된다

타입스크립트 넓히기 과정을 제어할 수 있는 방법

  • const
const x = "x"; // 타입이 'x'이다. let과 다르게 const는 타입 체커가 통과됨
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x);

하지만, const는 만능이 아니다. 앞의 const mixed 예제에서만 봐도 배열에 대한 문제가있음.

객체에서의 타입스크립트 넓히기 알고리즘은 각 요소를 let으로 할당한 것과 같다.

const v = {
  x: 1,
}; // x:number 타입으로 추론
v.x = 3; // 성공
v.x = "3"; // 에러
v.y = 4; // 에러
v.name = "name"; // 에러

타입 추론의 강도를 직접 제어하기 위해서는 기본 동작을 재정의 해야함!

기본동작 재정의 하는 3가지 방법

  1. 명시적 타입 구문 제공
const v: { x: 1 | 3 | 5 } = {
  x: 1,
};
  1. 타입 체커에 추가적인 문맥 제공 (함수 매개변수로 값을 전달)
  2. const 단언문 사용
const v1 = {
  x: 1,
  y: 2,
}; // x: number , y:number

const v2 = {
  x: 1 as const,
  y: 2,
}; // x:1; y:number

const v3 = {
  x: 1,
  y: 2,
} as const; // readonly x: 1, readonly y:2

const a1 = [1, 2, 3]; // number[]
const a2 = [1, 2, 3] as const; // readonly [1,2,3]

타입 좁히기의 가장 대표적인 예시는 null 체크
ex)if문으로 null체크하는 방법

타입 좁히기의 방법은
예외를 던지는 방법, instanceof를 사용, 속성 체크, 내장 함수 등으로 좁힐 수 있다.

예외를 던지는 방법

const el = document.getElementById("foo");
if (!el) throw new Error("Error!!!");
el;
el.innerHTML = "hello";

instancof사용

function contains(text: string, search: string | RegExp) {
  if (search instanceof RegExp) {
    search;
    return !!search.exec(text);
  }
  search;
  return text.includes(search);
}

속성 체크

interface A {
  a: number;
}
interface B {
  b: number;
}
function pickAB(ab: A | B) {
  if ("a" in ab) {
    ab; // 타입이 A
  } else {
    ab; // 타입이 B
  }
  ab; // 타입이 A | B
}

내장 함수

function contains(text: string, terms: string | string[]) {
  const termList = Array.isArray(terms) ? terms : [terms];
  termList; // string[]
}

자바스크립트에서 typeof null은 object이므로

const el = document.getElementById("foo");
if (typeof el === "object") {
  el;
} // 타입이 따로 좁혀지지 않는다.

명시적으로 태그를 붙이는 방법

interface UploadEvent {type: 'upload'; filname:string; contents: string}
interface DownloadEvent {type: 'download'; filname:string; }

type AppEvent = UploadEvent | DownloadEvent

function handleEvent(e: AppEvent) {
  switch(e.type) {
    case 'download':
    e // type이 DownloadEvent
    break;
    case 'upload'
    e // type이 UploadEvent
    break;
  }
}

사용자 정의 타입가드

const group = ["a", "b", "c", "d", "e"];
const members = ["f", "e"].map((who) => group.find((n) => n === who)); // type이 const members: (string | undefined)[]

function isDefined<T>(x: T | undefined): x is T {
  return x !== undefined;
}

const members = ["f", "e"]
  .map((who) => group.find((n) => n === who))
  .filter(isDefined); // type이 const members: string[]

타입추론에 유리하기 위해서는 객체를 생성시에 한꺼번에 생성해야 유리하다.

const pt = {};
pt.x = 3; // 오류가 발생

// 아래와같이 한꺼번에 만들기!
interface Point {
  x: number;
  y: number;
}
const pt: Point = {
  x: 3,
  y: 4,
};

객체 전개 연산자 활용하면 타입 걱정 없이 필드 단위로 객체를 생성 할 수도 있다.

const pt = { x: 3, y: 4 };
const id = { name: "abc" };

const namedPoint = {};
Object.assign(namedPoint, pt, id); // 이렇게 쓰는 것 보다

namedPoint.name; // 에러

const namedPoint = { ...pt, ...id }; // 전개연산자를 활용해야한다.
namedPoint.name; // 정상

객체나 배열을 변환해서 새로운 객체나 배열 생성시엔 루프 대신에 내장함수나 Lodash 활용하기!

interface Coordinate {
  x: number;
  y: number;
}

interface BoundingBox {
  x: [number, number];
  y: [number, number];
}

interface Polygon {
  exterior: Coordinate[];
  holes: Coordinate[][];
  bbox?: BoundingBox;
}

function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  if (polygon.bbox) {
    if (
      pt.x < polygon.bbox.x[0] ||
      pt.x > polygon.bbox.x[1] ||
      pt.y < polygon.bbox.y[0] ||
      pt.y > polygon.bbox.y[1]
    ) {
      return false;
    }
  }
  // ...
}

// 코드의 중복을 줄이기 위해 위에는 정상동작하지만, 아래와 같이 변경하면

function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const box = polygon.bbox;
  if (polygon.bbox) {
    if (
      pt.x < box.x[0] ||
      pt.x > box.x[1] ||
      pt.y < box.y[0] ||
      pt.y > box.y[1]
    ) {
      // 이렇게 되면 에러가 발생 객체가 undefined일수도있다.
      return false;
    }
  }
  // ...
}

function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const box = polygon.bbox;
  if (box) {
    // box로 바꾸는 것 만으로 에러가 해결
    if (
      pt.x < box.x[0] ||
      pt.x > box.x[1] ||
      pt.y < box.y[0] ||
      pt.y > box.y[1]
    ) {
      // 이렇게 되면 에러가 발생 객체가 undefined일수도있다.
      return false;
    }
  }
  // ...
}

// 객체 비구조화를 활용하여 간결한 문법으로 만들 수 있다.
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const { bbox } = polygon;
  if (bbox) {
    const { x, y } = bbox;
    if (pt.x < x[0] || pt.x > x[1] || pt.y < y[0] || pt.y > y[1]) {
      // 이렇게 되면 에러가 발생 객체가 undefined일수도있다.
      return false;
    }
  }
  // ...
}
function timeout(millis: number): Promise<never> {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject("timeout"), millis);
  });
}

async function fetchWithTimeout(url: string, ms: number) {
  return Promise.race([fetch(url), timeout(ms)]);
}

// 타입 구문이 없어도 반환 타입은  Promise<Response>이다.

Promise도 좋지만, async await를 사용하기

  • 코드가 간결하고 직관적
  • async함수는 항상 프로미스를 반환하도록 강제함
function getNumber(): Promise<number>;
async function getNumber() {
  return 42;
}
const getNumber = async () => 42; // 간단한 형태로 만들수있음
type Language = "Javascript" | "TypeScript" | "Python";
function setLanguage(language: Language) {}

setLanguage("Javascript"); // 정상

let language = "Javascript";
setLanguage(language); // 에러 , Argument of type 'string' is not assignable to parameter of type 'Language'
// 변수로 분리해내면 , 타입스크립트는 할당 시점에 타입을 추론한다.

let language: Language = "Javascript"; // 이렇게 타입 선언에서 가능한 값을 제한하거나
setLanguage(language);

const language = "Javascript"; // let 대신에 const 를 사용하여 타입 체커에게 language 는 변경불가라고 알려줌
setLanguage(language);

튜플 사용 시 주의점

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

panTo([10, 20]);

const loc = [10, 20];
panTo(loc); // 에러 발생 , Argument of type 'number[]' is not assignable to parameter of type '[number, number]'.

//아래와 같이 수정 가능

const loc: [number, number] = [10, 20]; // 타입 선언
panTo(loc); // 정상

// 타입 단언을 사용할 수 있긴하지만,
const loc = [10, 20] as const;
panTo(loc); // 에러 발생,  Argument of type 'readonly [10, 20]' is not assignable to parameter of type '[number, number]'.

// 함수를 수정하는게 낫다.
function panTo(where: readonly [number, number]) {}

// 하지만, 타입 정의에 실수가 있었다고 한다면,  오류가 발생할 수 있다.

객체 사용시 주의점

type Language = "Javascript" | "TypeScript" | "Python";

interface GovernedLanguage {
  lanuage: Language;
  organize: string;
}

function complain(language: GovernedLanguage) {}

complain({ lanuage: "TypeScript", organize: "microsoft" });

const ts = {
  language: "Typescript",
  organize: "microsoft",
};
// 타입 추론
// const ts: {
//     language: string;
//     organize: string;
// }

complain(ts); // 에러 발생, Argument of type '{ language: string; organize: string; }' is not assignable to parameter of type 'GovernedLanguage'.

const ts: GovernedLanguage = {
  // 타입선언 추가해줘야함
  language: "Typescript",
  organize: "microsoft",
};

콜백 사용시 주의점

function callWithRandomNunbers(fn: (n1: number, n2: number) => void) {
  fn(Math.random(), Math.random());
}

callWithRandomNunbers((a, b) => {
  a; // 타입 number
  b; // 타입 number
  console.log(a + b);
});

const fn = (a, b) => {
  // 에러 발생 Parameter 'a' implicitly has an 'any' type., Parameter 'b' implicitly has an 'any' type.
  console.log(a + b);
};
callWithRandomNunbers(fn);

const fn = (a: number, b: number) => {
  // 매개변수에 타입 구문을 추가해 해결 가능
  console.log(a + b);
};
callWithRandomNunbers(fn);

자바스크립트에 비해서 타입스크립트로 코드를 작성시에 서드파티 라비브러리를 사용하는것이 무조건 유리하다.
타입 정보를 참고하며 작업할 수 있기때문에! 코드도 간결해지고, 시간도 단축!

interface BasketballPlayer {
  name: string;
  team: string;
  salary: number;
}

declare const rosters: { [team: string]: BasketballPlayer[] };

// 반복문을 사용한 단순 목록 생성시

let allPlayers = []; // 에러 발생, ariable 'allPlayers' implicitly has type 'any[]' in some locations where its type cannot be determined.
for (const players of Object.values(rosters)) {
  allPlayers = allPlayers.concat(players); // 에러 발생, Variable 'allPlayers' implicitly has an 'any[]' type.
}

let allPlayers: BasketballPlayer[] = []; //  타입선언으로 오류해결
for (const players of Object.values(rosters)) {
  allPlayers = allPlayers.concat(players);
}

// 하지만 더 나은 해법으로

const allPlayers = Object.values(rosters).flat(); //오류가난다. flat을 지원하지않는것 같음.

lodash같은 라비르러리나, 내장된 함수형 기법을 통하여 타입스크립트를 안전하게 사용할 수 있다.

반응형