바로 직전에 Beginner's TypeScript Tutorial의 tutorial문제 1~9번까지의 풀이에 이은 나머지 문제 풀이.
지난 포스팅 TypeScript Tutorial 1~9번 문제 풀이 보러가기
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)를 하여도 에러가 발생하지 않는다.const guitarists = new Set<string>();
Set
의 형태를 보면,interface Set<T/> { //... }
와 같이 하나의 타입 매개변수가 들어갈 수 있는 구조임을 확인 할 수 있다.
Map
의 경우엔 두개의 타입 매개변수를 요한다. //Map<string, numbe>
와 같은 형태
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
라는 변수는 {}
만 할당되고 기타 정보가 없음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,
};
};
또 다른 해결방법
- ⭐
Record<K, T>
사용하기
Record<K, T>를 활용하면 key와 value값을 제네릭으로 받아와 새로운 {key: value}를 반환시켜준다.const cache: Record<string, string> = {}
type alias
사용하기
이는 interface와 마찬가지로 사용하면 된다.
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값을 달리 보내주도록 하였다.아래와 같이 작성하여도 test코드는 통과할 수는(!) 있다.
const coerceAmount = (amount: number | { amount: number }) => { if (amount.amount) { return amount.amount } return amount };
그러나 typescript에서는 에러를 띄운다. 그 이유는 명확하지 않은 타입에 대해선 접근할 수 없기 때문에 amount.amount와 같은 코드에 에러를 띄우는 것이다.
그러니 우리는 내로잉을 통해 typescript가 잘 이해할 수 있도록 해주자!
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;
}
}
};
Error
의 형태를 지니는지 먼저 체크한 후 그 뒤에 리턴값을 보내준다.기타 방법
any
로 퉁치자!catch (e: any) { return e.message; }
any
타입으로 설정해버리면 무난히 typescript의 경고를 무시할 수 있다. 하지만 TS를 조금 공부해본 사람이라면 any를 무분별하게 사용하는 것이 얼마나 안 좋은 것인지 알 수 있을 것이다.
as Error
로 강제 인식catch (e) { return (e as Error).message; }
위처럼
as
키워드를 통해 e 매개 변수가 Error의 형태를 취한다고 강제로 인식(?)시켜주는 방법이다.
이 방법으로도 해결이 되지만 가장 추천되는 방법이 아닌 이유는 위 코드에서 만약 try문에서 던져주는throw
문의 코드가new Error
가 아니라면 이를 확인하지 않고 그냥 실행해버리기 때문이다.
이 때문에
instanceof
연산자를 이용해 e의 타입을 체크한 뒤 리턴해주는, 보다 안전하게 확인하는 방법을 추천하는 것.
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 {
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를 확장하도록 하였다.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 => {/** ... */}
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 }>>];
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
💡힌트: 유틸리티 타입을 참고해보자.
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 제네릭에 넣어주어 해당 속성을 제외한 나머지 속성을 모두 지니는 새로운 객체 타입을 만들어 줄 수 있다.
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>>];
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
});
onFocusChange
함수를 사용하고자 한다.unknown
으로 설정되어 있어서 TS에러를 띄우고 있다.type Event = (params: boolean) => void
const addListener = (onFocusChange: Event) => {
window.addEventListener("focus", () => {
onFocusChange(true);
});
window.addEventListener("blur", () => {
onFocusChange(false);
});
};
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를 이제 막 공부한 사람이라면 한 번쯤 들어가 풀어보길 권장한다!