2024-06-01.md
๐กDIL: ์ดํํฐ๋ธ ํ์ ์คํฌ๋ฆฝํธ
์คํฐ๋: ์๊ฐ CS, https://github.com/monthly-cs/2024-05-effective-typescript
์์ฑ์ผ: 2024-06-01
์์ฑ์: dusunax
์์ดํ 35: ๋ฐ์ดํฐ๊ฐ ์๋, API์ ๋ช ์ธ๋ฅผ ๋ณด๊ณ ํ์ ๋ง๋ค๊ธฐ Avoid Types Based on Anecdotal Data
ํ๋ก์ ํธ ์ธ๋ถ์ ํ์
- ํ์ผ ํ์, API, ๋ช ์ธ specification
- ํ์ ์ ์์ฑํ์ง ์๊ณ ์๋ ์์ฑ (๋ช ์ธ๋ฅผ ์ฐธ๊ณ ํด์ ์์ฑ)
์์: DefinitelyTyped์ ํฌํจ๋ ํ์ ์ ์ ํ์ผ ์ฌ์ฉํ๊ธฐ
- ๋ผ์ด๋ธ๋ฌ๋ฆฌ: ๋ช ์ธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ์ ์ ์์ฑํด์, ํ์ฌ๊น์ง ๊ฒฝํํ ๋ฐ์ดํฐ ๋ฟ๋ง ์๋๋ผ ๋ชจ๋ ๊ฐ์ ๋ํด ์๋ํ๋ค๋ ํ์ ์ ๊ฐ์ง ์ ์๋ค.
- API: API ๋ช ์ธ๋ก๋ถํฐ ํ์ ์ ์์ฑํ ์ ์๋ค๋ฉด ๊ทธ๋ ๊ฒ ํ๊ธฐ.
- GraphQL
- ์์ฒด์ ์ผ๋ก ํ์ ์ด ์ ์ ์๋ API
- ํ์ ์คํฌ๋ฆฝํธ์ ๋น์ทํ ํ์ ์์คํ ์ฌ์ฉ. ๋ชจ๋ ์ฟผ๋ฆฌ์ ์ธํฐํ์ด์ค ๋ช ์ธํ๋ ์คํค๋ง๋ก ์ด๋ฃจ์ด์ง
- ํน์ ์ฟผ๋ฆฌ์ ํ์ ์ ์์ฑํ ์ ์๋ค. (null ์ฌ๋ถ ๋ชจ๋ธ๋ง)
// ๋ช
์์ ์ฐจ๋จ + ์ค๋ฅ ๋ฐ์
const { geometry } = f;
if (geometry) {
if (geometry.type === "GeometryCollection") {
// ํ๊ทธ๋ ์ ๋์จ
// GeometryCollection๋ฅผ ๋ช
์์ ์ผ๋ก ์ฐจ๋จ, ์๋ฌ throw!
throw new Error("GeometryCollections are not supported.");
}
// ์ ์ ๋ ํ์
์ ํํด์ ์ฐธ์กฐ๋ฅผ ํ์ฉ
helper(geometry.coordinates); // OK
}
// ์กฐ๊ฑด ๋ถ๊ธฐ + ํฌํผ ํจ์ ๋ถ๋ฆฌ
const geometryHelper = (g: Geometry) => {
if (g.type === "GeometryCollection") {
g.geometries.forEach(geometryHelper);
} else {
helper(g.coordinates); // OK
}
};
const { geometry } = f;
if (geometry) {
geometryHelper(geometry);
}
GraphQL
query getLicense($owner:String!, $name:String!){ // GraphQL์ String ํ์
. nullable์ด๋ผ null ์๋ ๋จ์ธ
repository(owner: $owner, name: $name){
description
licenseInfo {
spdxId
name
}
}
}
Apollo
- graphQL ์ฟผ๋ฆฌ๋ฅผ ํ์ ์คํฌ๋ฆฝํธ ํ์ ์ผ๋ก ๋ณํํด์ฃผ๋ ๋๊ตฌ
$ apollo client:codegen \ // Apollo CLI์ ์ฝ๋ ์์ฑ ๋ช
๋ น์ด
--endpoint https://api.github.com/graphql \ // endpoint ์๊ธฐ์ ์คํค๋ง๋ฅผ ์ป์
--include license.graphql \ // ํ์
์์ฑ์ ํฌํจํ GraphQL ํ์ผ
--target typescript // TypeScript ํ์
์ ์๋ก ๋ณํ
- codegen
- ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์์ ์๋ต ์ธํฐํ์ด์ค ์์ฑ
- ์ฃผ์์ JSDoc์ผ๋ก ๋ณํํ์ฌ ํธ์ง๊ธฐ์์ ํ์ธํ ์ ์๋๋ก ํจ
๋ช ์ธ ์ ๋ณด๋ ๊ณต์ ์คํค๋ง๊ฐ ์๋ค๋ฉด?
- ๋ฐ์ดํฐ๋ก๋ถํฐ ํ์
์ ์์ฑํด์ผํจ
- quicktype ๊ฐ์ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ ์ ์์ง๋ง, ์์ฑ๋ ํ์ ์ด ์ค์ ๋ฐ์ดํฐ์ ์ผ์นํ์ง ์์ ์ ์๋ค.
- ์ฐ๋ฆฌ๋ ์ด๋ฏธ ์๋ ํ์
์์ฑ์ ์ฌ์ฉํ๊ณ ์๋ค.
- ex) ๋ธ๋ผ์ฐ์ DOM API ํ์
- ๋ณต์กํ ์์คํ ์ ์ ํํ ๋ชจ๋ธ๋งํ๊ณ , ํ์ ์คํฌ๋ฆฝํธ๊ฐ ์ค๋ฅ๋ ์ค์๋ฅผ ์ก์ ์ ์๋๋ก ํ๊ธฐ
์์ดํ 36: ํด๋น ๋ถ์ผ์ ์ฉ์ด๋ก ํ์ ์ด๋ฆ ์ง๊ธฐ Name Types Using the Language of Your Problem Domain
- (Two Hard Things) There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton
Naming
- ์ด๋ฆ ์ง๊ธฐ๋ ํ์
์ค๊ณ์์ ์ค์ํจ
- ์๋๋ฅผ ๋ช ํํ ํ๊ณ , ์ถ์ํ ์์ค์ ๋์ธ๋ค
- ์์ฑ์ ๋ํ ์๋ฏธ๋ฅผ ๋ถ๋ช ํ๊ฒ ํ๊ธฐ
// ๋ชจํธํ ํ์
interface Animal {
name: string; // ์ด๋ฆ์ธ์ง ํ๋ช
์ธ์ง?
endangered: boolean; // ๋ฉธ์ข
/ ๋ฉธ์ข
์๊ธฐ / ๋ฉธ์ข
์๊ธฐ ์๋
habitat: string; // ๋ฒ์๊ฐ ๋๋ฌด ๋๋ค. ๋ป์ด ๋ชจํธํจ
}
// ๋ถ๋ช
ํ๊ฒ ํ๊ธฐ
interface Animal {
commonName: string;
genus: string;
species: string;
status: ConservationStatus;
climates: KoppenClimate[];
}
type ConservationStatus = "EX" | "EW" | "CR" | "EN" | "VU" | "NT" | "LC";
type KoppenClimate =
| "Af"
| "Am"
| "As"
| "Aw"
| "BSh"
| "BSk"
| "BWh"
| "BWk"
| "Cfa"
| "Cfb"
| "Cfc"
| "Csa"
| "Csb"
| "Csc"
| "Cwa"
| "Cwb"
| "Cwc"
| "Dfa"
| "Dfb"
| "Dfc"
| "Dfd"
| "Dsa"
| "Dsb"
| "Dsc"
| "Dwa"
| "Dwb"
| "Dwc"
| "Dwd"
| "EF"
| "ET";
const snowLeopard: Animal = {
commonName: "Snow Leopard", // ์ผ๋ฐ์ ์ธ ๋ช
์นญ
genus: "Panthera", // ํ๋ช
species: "Uncia", // ์ข
status: "VU", // ๋๋ฌผ ๋ณดํธ ๋ฑ๊ธ์ ๋ํ IUGN์ ํ์ค ๋ถ๋ฅ ์ฒด๊ณ์ธ ConservationStatus ํ์
(์ทจ์ฝ์ข
Vulnerable)
climates: ["ET", "EF", "Dfd"], // ์พจํ ๊ธฐํ ๋ถ๋ฅ (๊ณ ์ฐ๋ alpine, ์๊ณ ์ฐ๋ subalpine)
};
์ ๋
- ์์ฒด์ ์ผ๋ก ์ฉ์ด๋ฅผ ๋ง๋ค์ง ๋ง๊ณ , ํด๋น ๋ถ์ผ์ ์ด๋ฏธ ์กด์ฌํ๋ ์ฉ์ด๋ฅผ ์ฌ์ฉํ๋ค
- ์ค๋ซ๋์ ๋ค๋ฌ์ด์ ธ ์์ผ๋ฉฐ, ํ์ฅ์์ ์ฌ์ฉ๋๊ณ ์์
- ์ฌ์ฉ์์์ ์ํต์ ์ ๋ฆฌํ๊ณ ํ์ ์ ๋ช ํ์ฑ์ ์ฌ๋ฆด ์ ์๋ค
- ์ ๋ฌธ ๋ถ์ผ์ ์ฉ์ด๋ ์ ํํ๊ฒ ์ฌ์ฉํด์ผ ํ๋ค.
Todo
- ๋์ผํ ์๋ฏธ๋ฅผ ๋ํ๋ผ ๋, ๊ฐ์ ์ฉ์ด ์ฌ์ฉํ๊ธฐ
- ๋ชจํธํ๊ณ ์๋ฏธ ์๋ ์ด๋ฆ ํผํ๊ธฐ
- ex) data, info, thing, item, object, entity
- ๋ฐ์ดํฐ ์์ฒด๊ฐ ๋ฌด์์ธ์ง ๊ณ ๋ คํ๊ธฐ
- ex) INodeLis๊ฐ ์๋๋ผ Directory. ์ถ์ํ์ ์์ค ๋์ฌ์ ์ถฉ๋ ์ํ์ฑ ์ค์ด๊ธฐ
Things to Remember
- Reuse names from the domain of your problem where possible to increase the readability and level of abstraction of your code. Make sure you use domain terms accurately.
- ๊ฐ๋ ์ฑ๊ณผ ์ถ์ํ๋ฅผ ๋์ด๋ ์ด๋ฆ์ ์ฌ์ฌ์ฉํ๊ณ , ๋๋ฉ์ธ์ ์ฉ์ด๋ฅผ ์ ํํ๊ฒ ์ฌ์ฉํ๊ธฐ
- Avoid using different names for the same thing: make distinctions in names meaningful.
- ๊ฐ์ ์๋ฏธ์ ๋ค๋ฅธ ์ด๋ฆ ๋ถ์ด์ง ์๊ธฐ. ์ฉ์ด๋ฅผ ๊ตฌ๋ถํ๊ธฐ
- Avoid vague names like "Info" or "Entity." Name types for what they are, rather than for their shape.
- ๋ชจํธํ ์ด๋ฆ ๋ถ์ด์ง ์๊ธฐ. ๋ฐ์ดํฐ ํํ๋ณด๋ค, ๋ฐ์ดํฐ ์์ฒด๊ฐ ๋ฌด์์ธ์ง ๊ณ ๋ คํ๊ธฐ
์์ดํ 37: ๊ณต์ ๋ช ์นญ์๋ ์ํ๋ฅผ ๋ถ์ด๊ธฐ Consider Brands for Nominal Typing
- ๊ตฌ์กฐ์ ํ์ดํ ํน์ฑ ๋๋ฌธ์ ์ฝ๋๊ฐ ์ด์ํ ๊ฒฐ๊ณผ๊ฐ ๋์ค๋ ๊ฒฝ์ฐ๊ฐ ์๋ค.
interface Vector2D {
x: number;
y: number;
}
function calculateNorm(p: Vector2D) {
return Math.sqrt(p.x ** 2 + p.y ** 2);
}
calculateNorm({ x: 3, y: 4 }); // OK, result is 5
const vec3D = { x: 3, y: 4, z: 1 };
calculateNorm(vec3D); // OK! result is also 5 // ํ์ง๋ง ์ค๋ฅ๊ฐ ๋๋ ๊ฒ์ด ์ํ์ ์ผ๋ก ์ด์น์ ๋ง๋ค.
๋ช ๋ชฉ ํ์ /๊ณต์ ๋ช ์นญ Nominal typing
- ๊ฐ์ ํ์
์ด ๊ตฌ์กฐ๊ฐ ์๋ ๋ช
์์ ์ธ ์ ์ธ์ ์ํด ๊ฒฐ์ ๋๋ ์์คํ
- ๊ตฌ์กฐ์ ํ์ดํ(๊ตฌ์กฐ์ ๋ฐ๋ผ ํ์ ์ด ๊ฒฐ์ ๋๋ ์์คํ )๊ณผ ๋์กฐ๋๋ค.
Branding ์ํ ๊ธฐ๋ฒ
_brand
,__brand
์์A: ์ํ๋ก ํ์ ์ ์ ํ๊ธฐ
type AbsolutePath = string & { _brand: "abs" };
function listAbsolutePath(path: AbsolutePath) {
// ...
}
function isAbsolutePath(path: string): path is AbsolutePath {
return path.startsWith("/");
}
console.log(isAbsolutePath("order/orderId")); // false
console.log(isAbsolutePath("/order/orderId")); // true
- string์ด๋ฉด์ {_brand: 'abs'}๋ฅผ ๊ฐ์ง๋ ๊ฐ์ฒด๋ฅผ ์์ฑํ ์๋ ์๋ค! (์์ ํ ํ์ ์์คํ ์์ญ)
function f(path: string) {
if (isAbsolutePath(path)) {
// if ์ฒดํฌ๋ก ํ์
์ ์ ์ refine, ๋จ์ธ๋ฌธ์ ์ง์ํ์
listAbsolutePath(path); // AbsolutePath
}
listAbsolutePath(path); // AbsolutePath ์๋
// ~~~~ Argument of type 'string' is not assignable to
// parameter of type 'AbsolutePath'
}
์์B
- ๋ชฉ๋ก์์ ํ ์์๋ฅผ ์ฐพ๊ธฐ ์ํ ์ด์ง ๊ฒ์
- ๋ชฉ๋ก์ด ์ ๋ ฌ๋์ด์ผ ์์ด์ผ ํ๋ค.
function binarySearch<T>(xs: T[], x: T): boolean {
let low = 0,
high = xs.length - 1;
while (high >= low) {
const mid = low + Math.floor((high - low) / 2);
const v = xs[mid];
if (v === x) return true;
[low, high] = x > v ? [mid + 1, high] : [low, mid - 1];
}
return false;
}
// ์ ๋ ฌ๋ ๋ฆฌ์คํธ์ธ์ง ํ์ธํ๊ธฐ ์ํด ์ํ ๊ธฐ๋ฒ์ ์ฌ์ฉํ ๊ฒ
type SortedList<T> = T[] & { _brand: "sorted" };
function isSorted<T>(xs: T[]): xs is SortedList<T> {
for (let i = 0; i < xs.length - 1; i++) {
if (xs[i] > xs[i + 1]) {
return false;
}
}
return true;
}
function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
let low = 0,
high = xs.length - 1;
while (high >= low) {
const mid = low + Math.floor((high - low) / 2);
const v = xs[mid];
if (v === x) return true;
[low, high] = x > v ? [mid + 1, high] : [low, mid - 1];
}
return false;
}
const array = [2, 0, 5];
isSorted(array) && binarySearch(array, 1);
binarySearch(array, 1); // ์ค๋ฅ!
์์C: ์ํ ๋ถ์ด๊ธฐ & ์ฐ์ฐ ํ ์ฌ๋ผ์ง๋ ๊ฒฝ์ฐ(์ซ์)
- number ํ์
์ ์ํ๋ฅผ ๋ถ์ฌ๋, ์ฐ์ ์ฐ์ฐ ํ ์ํ๊ฐ ์์ด์ง๊ธฐ ๋๋ฌธ์ ์ฌ์ฉํ๊ธฐ ๋ฌด๋ฆฌ๊ฐ ์๋ค.
- ํ์ง๋ง ํผํฉ๋ ๋ง์ ์์ number๊ฐ ์์ ๋๋, ๋จ์๋ฅผ ๋ฌธ์ํํ๊ธฐ ์ํด ์ฌ์ฉํ ์ ์์
type Meters = number & { _brand: "meters" }; // numbe์ธ๋ฐ _brand๊ฐ "meters"
type Seconds = number & { _brand: "seconds" }; // numbe์ธ๋ฐ _brand๊ฐ "seconds"
const meters = (m: number) => m as Meters;
const seconds = (s: number) => s as Seconds;
const oneKm = meters(1000);
// ^? const oneKm: Meters
const oneMin = seconds(60);
// ^? const oneMin: Seconds
const tenKm = oneKm * 10;
// ^? const tenKm: number
const v = oneKm / oneMin; // ํ์
์์ด์ง
// ^? const v: number
๋ธ๋๋ฉ์ ๋ค์ํ ๊ธฐ๋ฒ
object types
type UserID = { value: number; __brand: "UserID" };
type OrderID = { value: number; __brand: "OrderID" };
function createUserID(value: number): UserID {
return { value, __brand: "UserID" };
}
function createOrderID(value: number): OrderID {
return { value, __brand: "OrderID" };
}
const userID: UserID = createUserID(1);
const orderID: OrderID = createOrderID(2);
// ํ์
์ค๋ฅ: UserID์ OrderID๋ ์๋ก ํธํ๋์ง ์์ต๋๋ค.
const anotherOrderID: OrderID = userID;
๋ฌธ์์ด ๊ธฐ๋ฐ ์ด๊ฑฐํ string-based enums
enum Brand {
UserID = "UserID",
OrderID = "OrderID",
}
type UserID = { value: number; brand: Brand.UserID };
type OrderID = { value: number; brand: Brand.OrderID };
function createUserID(value: number): UserID {
return { value, brand: Brand.UserID };
}
function createOrderID(value: number): OrderID {
return { value, brand: Brand.OrderID };
}
const userID: UserID = createUserID(1);
const orderID: OrderID = createOrderID(2);
// ํ์
์ค๋ฅ: UserID์ OrderID๋ ์๋ก ํธํ๋์ง ์์ต๋๋ค.
const anotherOrderID: OrderID = userID;
private fields
class UserID {
private __brand: "UserID" = "UserID"; // ํด๋์ค์ ํ๋ผ์ด๋น ํ๋ ์ฌ์ฉ
constructor(public value: number) {}
}
class OrderID {
private __brand: "OrderID" = "OrderID";
constructor(public value: number) {}
}
const userID = new UserID(1);
const orderID = new OrderID(2);
// ํ์
์ค๋ฅ: UserID์ OrderID๋ ์๋ก ํธํ๋์ง ์์ต๋๋ค.
const anotherOrderID: OrderID = userID; // ํ๋ผ์ด๋น ํ๋๊ฐ ์์ผ๋ฉด ์ค๋ฅ๊ฐ ๋ฐ์ํ์ง ์์
unique symbols
class UserID {
private __brand: "UserID" = "UserID";
constructor(public value: number) {}
}
class OrderID {
private __brand: "OrderID" = "OrderID";
constructor(public value: number) {}
}
const userID = new UserID(1);
const orderID = new OrderID(2);
// ํ์
์ค๋ฅ: UserID์ OrderID๋ ์๋ก ํธํ๋์ง ์์ต๋๋ค.
const anotherOrderID: OrderID = userID;
Things to Remember
- With nominal typing, a value has a type because you say it has a type, not because it has the same shape as that type.
- ๋ช ๋ชฉ ํ์ > ์ค์ ๊ฐ์ ๊ฐ์ง๊ณ ์๊ธฐ ๋๋ฌธ์ด ์๋๋ผ, ํด๋น ํ์ ์ด๋ผ๊ณ ํ๊ธฐ ๋๋ฌธ์ ๊ทธ ํ์
- ๊ตฌ์กฐ์ ํ์ดํ <-> ๋ช ๋ชฉ ํ์
- Consider attaching brands to distinguish primitive and object types that are semantically distinct but structurally identical.
- ์์ํ์ด๋ ์ค๋ธ์ ํธ ํ์ ์ด ์๋ฏธ๊ฐ ๋ค๋ฅด์ง๋ง ๊ตฌ์กฐ๊ฐ ๋์ผํ ๊ฒฝ์ฐ, ๋ธ๋๋ฉ์ ๊ณ ๋ คํ์.
- ํ์ ์คํฌ๋ฆฝํธ๋ ๊ตฌ์กฐ์ ํ์ดํ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์, ๊ฐ์ ์ธ๋ฐํ๊ฒ ๊ตฌ๋ถํ์ง ๋ชปํ๋ ์
- ํ์ ์์คํ ์์ ๋์ํ์ง๋ง ๋ฐํ์์ ์ํ๋ฅผ ๊ฒ์ฌํ๋ ๊ฒ๊ณผ ๋์ผํ ํจ๊ณผ๋ฅผ ์ป์ ์ ์๋ค.
- Be familiar with the various techniques for branding: properties on object types, string-based enums, private fields, and unique symbols.
- ๋ธ๋๋ฉ์ ๋ค์ํ ๊ธฐ๋ฒ์ ์์๋๊ธฐ