[스터디] Beginner's TypeScript 풀이하기(2)

정(JJeong)·2023년 5월 9일
0

스터디 기록

목록 보기
5/5
post-thumbnail

바로 직전에 Beginner's TypeScript Tutorial의 tutorial문제 1~9번까지의 풀이에 이은 나머지 문제 풀이.

지난 포스팅 TypeScript Tutorial 1~9번 문제 풀이 보러가기



10) Passing Type Arguments

문제

import { expect, it } from "vitest";
import { Equal, Expect } from "./helpers/type-utils";

const guitarists = new Set();

guitarists.add("Jimi Hendrix");
guitarists.add("Eric Clapton");

it("Should contain Jimi and Eric", () => {
  expect(guitarists.has("Jimi Hendrix")).toEqual(true);
  expect(guitarists.has("Eric Clapton")).toEqual(true);
});

it("Should give a type error when you try to pass a non-string", () => {
  // @ts-expect-error
  guitarists.add(2);
});

it("Should be typed as an array of strings", () => {
  const guitaristsAsArray = Array.from(guitarists);

  type tests = [Expect<Equal<typeof guitaristsAsArray, string[]>>];
  					// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
});
  • new Set()을 통해 만든 guitarists변수에 add메소드를 통해 string값을 넣어주었다.
  • 기대했던 것과 달리 guitarists변수의 타입은 Set<string>이 아닌 Set<unknown>이다. 때문에 add(2)를 하여도 에러가 발생하지 않는다.
  • string으로만 guitarists가 구성되도록 이를 해결해보자.

풀이

const guitarists = new Set<string>();
  • 타입 매개변수(type argument)를 지정해서 해결

Set의 형태를 보면,

interface Set<T/> { //... }

와 같이 하나의 타입 매개변수가 들어갈 수 있는 구조임을 확인 할 수 있다.
Map의 경우엔 두개의 타입 매개변수를 요한다. // Map<string, numbe>와 같은 형태


11) Assigning Dynamic Keys to an Object

문제

import { expect, it } from 'vitest';

const createCache = () => {
  const cache = {};

  const add = (id: string, value: string) => {
    cache[id] = value;
 // ~~~~~~~~~
  };

  const remove = (id: string) => {
    delete cache[id];
    	// ~~~~~~~~~
  };

  return {
    cache,
    add,
    remove,
  };
};

it('Should add values to the cache', () => {
  const cache = createCache();

  cache.add('123', 'Matt');

  expect(cache.cache['123']).toEqual('Matt');
  //	 ~~~~~~~~~~~~~~~~~
});

it('Should remove values from the cache', () => {
  const cache = createCache();

  cache.add('123', 'Matt');
  cache.remove('123');

  expect(cache.cache['123']).toEqual(undefined);
  //	 ~~~~~~~~~~~~~~~~~
});
  • cache라는 변수는 {}만 할당되고 기타 정보가 없음
    • string의 key와 value모두 할당이 되지 않는다.
    • string의 key로 인덱싱할 수 없어 타입 any로 해석된다.

풀이

interface Cache {
  [key: string]: string;
}

const createCache = () => {
  const cache: Cache = {};

  const add = (id: string, value: string) => {
    cache[id] = value;
  };

  const remove = (id: string) => {
    delete cache[id];
  };

  return {
    cache,
    add,
    remove,
  };
};
  • interface를 활용해 cache객체의 타입을 정해주었음
    • 인덱스 시그니쳐를 활용해 key의 타입이 string이 되도록 하였다.

또 다른 해결방법

  1. Record<K, T>사용하기
    Record<K, T>를 활용하면 key와 value값을 제네릭으로 받아와 새로운 {key: value}를 반환시켜준다.
const cache: Record<string, string> = {}

이전 Record에 대한 코드 간략 정리글

  1. type alias 사용하기
    이는 interface와 마찬가지로 사용하면 된다.

12) Narrowing Down Union Types

문제

import { expect, it } from "vitest";

const coerceAmount = (amount: number | { amount: number }) => {};

it("Should return the amount when passed an object", () => {
  expect(coerceAmount({ amount: 20 })).toEqual(20);
});

it("Should return the amount when passed a number", () => {
  expect(coerceAmount(20)).toEqual(20);
});
  • 유니온 타입으로 설정된 매개 변수로 인해 정확한 결과가 나오지 않고 있다.
  • 유니온 타입의 각 경우에 따른 결과를 일치시킬 수 있도록 만들어보자.

풀이

const coerceAmount = (amount: number | { amount: number }) => {
  if (typeof amount === "number") {
    return amount
  } else if (typeof amount === "object") {
    return amount.amount
  }
};
  • typeof연산자를 이용한 타입 검사를 통해 내로잉을 거쳐 어떤 매개 변수냐에 따라 return값을 달리 보내주도록 하였다.
    • if문을 하나는 생략할 수도 있다, 그러나 필자는 정확히 하기 위해 둘다 조건문을 작성해주었다.

아래와 같이 작성하여도 test코드는 통과할 수는(!) 있다.

const coerceAmount = (amount: number | { amount: number }) => {
  if (amount.amount) {
    return amount.amount
  }
   return amount
};

그러나 typescript에서는 에러를 띄운다. 그 이유는 명확하지 않은 타입에 대해선 접근할 수 없기 때문에 amount.amount와 같은 코드에 에러를 띄우는 것이다.
그러니 우리는 내로잉을 통해 typescript가 잘 이해할 수 있도록 해주자!


13) Typing Errors in a Try-Catch✅

문제

import { expect, it } from "vitest";

const tryCatchDemo = (state: "fail" | "succeed") => {
  try {
    if (state === "fail") {
      throw new Error("Failure!");
    }
  } catch (e) {
    return e.message;
    //	   ~
  }
};

it("Should return the message when it fails", () => {
  expect(tryCatchDemo("fail")).toEqual("Failure!");
});
  • catch문에서 받을 수 있는 e(매개 변수)는 어느 것이든 받을 수 있어 unknown 상태이다.
  • 이 때문에 발생하는 에러를 해결하자.

풀이

✅ 필자는 여기서 막혀서 솔루션을 봤다..;

const tryCatchDemo = (state: "fail" | "succeed") => {
  try {
    if (state === "fail") {
      throw new Error("Failure!");
    }
  } catch (e) {
    if (e instanceof Error) {
      return e.message;
    }
  }
};
  • 가장 추천되는 방법이다. 매개변수 e가 Error의 형태를 지니는지 먼저 체크한 후 그 뒤에 리턴값을 보내준다.
    • 가장 추천되는 방법인 이유는 아래 경우를 통해 알아보자.

기타 방법

  1. any로 퉁치자!
catch (e: any) {
  return e.message;
}

any타입으로 설정해버리면 무난히 typescript의 경고를 무시할 수 있다. 하지만 TS를 조금 공부해본 사람이라면 any를 무분별하게 사용하는 것이 얼마나 안 좋은 것인지 알 수 있을 것이다.

  1. as Error로 강제 인식
catch (e) {
  return (e as Error).message;
}

위처럼 as키워드를 통해 e 매개 변수가 Error의 형태를 취한다고 강제로 인식(?)시켜주는 방법이다.
이 방법으로도 해결이 되지만 가장 추천되는 방법이 아닌 이유는 위 코드에서 만약 try문에서 던져주는 throw문의 코드가 new Error가 아니라면 이를 확인하지 않고 그냥 실행해버리기 때문이다.

이 때문에 instanceof연산자를 이용해 e의 타입을 체크한 뒤 리턴해주는, 보다 안전하게 확인하는 방법을 추천하는 것.


14) Inheriting Interface Properties

문제

import { Equal, Expect } from "./helpers/type-utils";

/**
 * Here, the id property is shared between all three
 * interfaces. Can you find a way to refactor this to
 * make it more DRY?
 */

interface User {
  id: string;
  firstName: string;
  lastName: string;
}

interface Post {
  id: string;
  title: string;
  body: string;
}

interface Comment {
  id: string;
  comment: string;
}

type tests = [
  Expect<Equal<User, { id: string; firstName: string; lastName: string }>>,
  Expect<Equal<Post, { id: string; title: string; body: string }>>,
  Expect<Equal<Comment, { id: string; comment: string }>>,
];
  • 위 세개의 interface는 모두 공통적인 id 속성을 지닌다.
  • 이러한 반복적인 코드를 개선할 순 없을까?

풀이

interface Id {
  id: string
}

interface User extends Id {
  firstName: string;
  lastName: string;
}

interface Post extends Id {
  title: string;
  body: string;
}

interface Comment extends Id {
  comment: string;
}
  • extends키워드를 이용해 공통적인 속성을 지닌 Id interface를 확장하도록 하였다.

15) Combining Types to Create New Types✅

문제

interface User {
  id: string;
  firstName: string;
  lastName: string;
}

interface Post {
  id: string;
  title: string;
  body: string;
}

/**
 * How do we type this return statement so it's both
 * User AND { posts: Post[] }
 */
export const getDefaultUserAndPosts = (): unknown => {
  return {
    id: "1",
    firstName: "Matt",
    lastName: "Pocock",
    posts: [
      {
        id: "1",
        title: "How I eat so much cheese",
        body: "It's pretty edam difficult",
      },
    ],
  };
};

const userAndPosts = getDefaultUserAndPosts();

console.log(userAndPosts.posts[0]);
//			~~~~~~~~~~~~
  • 현재 getDefaultUserAndPosts가 리턴하는 값은 User와 Post의 내용을 모두 지닌 또 다른 객체의 형태를 띄고 있다.
  • 그러나 이 함수의 리턴 타입을 unknown으로 지정해두었기 때문에 TS에러가 뜬다. 이를 해결해보자.

단, 다른 것은 건드리지 말고 함수의 리턴 타입만을 수정해 해결해보자.
즉, unknown만 수정할 것.


풀이

interface User {
  id: string;
  firstName: string;
  lastName: string;
}

interface Post {
  id: string;
  title: string;
  body: string;
}

interface UserAndPost extends User {
  posts: Post[]
}

export const getDefaultUserAndPosts = (): UserAndPost => {
  return {
    id: "1",
    firstName: "Matt",
    lastName: "Pocock",
    posts: [
      {
        id: "1",
        title: "How I eat so much cheese",
        body: "It's pretty edam difficult",
      },
    ],
  };
};
  • extends키워드를 통해 User를 확장하고 그 안에 posts라는 키의 값으로 Post[]타입을 지정해 주었다.
    • 문제에서 제시한 것은 리턴 타입만을 건드리는 것이었지만 다른 방법이 떠오르지 않아 이를 통해 해결했다.
    • 리턴 타입만을 건드려 해결하는 방법은 아래를 통해 보도록 하자.

리턴 타입만을 수정하여 해결하는 방법

export const getDefaultUserAndPosts = (): User & {posts: Post[]} => {/** ... */}

위에 보이는 바와 같이 &연산자를 이용해 손쉽게 두개의 타입을 합쳐낼 수 있다.
이를 가독성을 높이기 위해 아래와 같이 밖으로 빼낼 수도 있다.

type DefaultUserAndPosts = User & {posts: Post[]};

export const getDefaultUserAndPosts = (): DefaultUserAndPosts => {/** ... */}

16) Selectively Construct Types from Other Types✅

문제

import { Equal, Expect } from "./helpers/type-utils";

interface User {
  id: string;
  firstName: string;
  lastName: string;
}

/**
 * How do we create a new object type with _only_ the
 * firstName and lastName properties of User?
 */

type MyType = unknown;

type tests = [Expect<Equal<MyType, { firstName: string; lastName: string }>>];
//					 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  • User라는 interface에서 firstName과 lastName 속성만을 가져와 새로운 객체 타입을 만들고자 한다. 어떻게 해야할까?

💡힌트: 유틸리티 타입을 참고해보자.


풀이

type MyType = Pick<User, 'firstName' | 'lastName'>;

type tests = [Expect<Equal<MyType, { firstName: string; lastName: string }>>];
  • 유틸리티 연산자 중 하나인 Pick<T, K>를 이용하여 T 제네릭 값에 들어오는 타입의 속성 중 원하는 속성만 추출하여 새로운 객체 타입을 만들어 줄 수 있다.

또 다른 해결 방법

  • Omit<T, K>: Pick<T, K>의 반대 개념 이용하기
type MyType = Omit<User, 'id'>;

Pick의 반대 개념인 Omit을 이용하면 제외시킬 속성을 K 제네릭에 넣어주어 해당 속성을 제외한 나머지 속성을 모두 지니는 새로운 객체 타입을 만들어 줄 수 있다.


17) Typing Functions

문제

import { Equal, Expect } from "./helpers/type-utils";

/**
 * How do we type onFocusChange?
 */
const addListener = (onFocusChange: unknown) => {
  window.addEventListener("focus", () => {
    onFocusChange(true);
 // ~~~~~~~~~~~~~
  });

  window.addEventListener("blur", () => {
    onFocusChange(false);
 // ~~~~~~~~~~~~~
  });
};

addListener((isFocused) => {
  //		 ~~~~~~~~~
  console.log({ isFocused });

  type tests = [Expect<Equal<typeof isFocused, boolean>>];
  //				   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
});
  • focus이벤트에 따라 callback으로 받아온 onFocusChange함수를 사용하고자 한다.
  • 그러나 현재는 unknown으로 설정되어 있어서 TS에러를 띄우고 있다.
  • 매개변수로 들어오는 callback함수의 타입을 지정해 이를 해결해보자.

풀이

type Event = (params: boolean) => void

const addListener = (onFocusChange: Event) => {
  window.addEventListener("focus", () => {
    onFocusChange(true);
  });

  window.addEventListener("blur", () => {
    onFocusChange(false);
  });
};
  • Event라는 이름으로 함수 타입을 만들어 이를 onFocusChange의 타입으로 지정해주었다.
    • 매개 변수는 boolean값으로 지정하였고, 반환하는 값이 없으므로 void로 설정했다.

18) Typing Async Functions

문제

import { expect, it } from "vitest";

interface User {
  id: string;
  firstName: string;
  lastName: string;
}

const createThenGetUser = async (
  createUser: unknown,
  getUser: unknown,
): Promise<User> => {
  const userId: string = await createUser();

  const user = await getUser(userId);

  return user;
};

it("Should create the user, then get them", async () => {
  const user = await createThenGetUser(
    async () => "123",
    async (id) => ({
      id,
      firstName: "Matt",
      lastName: "Pocock",
    }),
  );

  expect(user).toEqual({
    id: "123",
    firstName: "Matt",
    lastName: "Pocock",
  });
});
  • async함수에 대한 타입을 지정해보자.

풀이

type CreateUserFunction = () => Promise<string>
type GetUerFucntion = (userId: string) => Promise<User>

const createThenGetUser = async (
  createUser: CreateUserFunction,
  getUser: GetUerFucntion,
): Promise<User> => {
  const userId: string = await createUser();

  const user = await getUser(userId);

  return user;
};
  • async함수는 반드시 Promise객체를 반환한다.
  • 이를 반영해 createThenGetUser라는 함수에 들어가는 두 개의 매개 변수 함수의 타입을 지정할 때 반환 값을 Promise로 작성한 뒤 타입을 지정해 주었다.


이렇게 Tutorial의 모든 문제를 풀어보았다.

확실히 그냥 이론과 이론에 대한 예문을 보기만 했던 것과 달리,
제시된 문제를 보면서, 이를 해결하기 위한 방법을 고민하는 과정을 통해 실제 어떻게 적용하면 좋을 지 생각해볼 수 있어서 좋았던 것 같다.

다시 한 번더 TS를 이제 막 공부한 사람이라면 한 번쯤 들어가 풀어보길 권장한다!

profile
프론트엔드 개발자를 꿈꾸며 기록하는 블로그 💻

0개의 댓글