2024-05-17.md

🏑

DIL: μ΄νŽ™ν‹°λΈŒ νƒ€μž…μŠ€ν¬λ¦½νŠΈ

μŠ€ν„°λ””: μ›”κ°„ CS, https://github.com/monthly-cs/2024-05-effective-typescript
μž‘μ„±μΌ: 2024-05-17
μž‘μ„±μž: dusunax

μ•„μ΄ν…œ 8 μ΄μ–΄μ„œ

쀑간 μ €μž₯~!~!
17일 λͺ©ν‘œ: ~12μž₯κΉŒμ§€
총 ν•™μŠ΅μ‹œκ°„: ν˜„μž¬ 1μ‹œκ°„ 50λΆ„

속성 μ ‘κ·Όμž [], property accessor

  • obj['field']와 obj.fieldλŠ” 값이 λ™μΌν•˜λ”λΌλ„ νƒ€μž…μ€ λ‹€λ₯Ό 수 μžˆλ‹€.
const jane: Person = { first: "Jane", last: "Jacobs" };
const first: Person["first"] = jane["first"]; // Or jane.first
//    ―――――                    ――――――――――――― Values. "Jane"
//           ―――――― ―――――――                  Types, νƒ€μž… λ§₯λ½μ—μ„œ μ“°μ˜€κΈ° λ•Œλ¬Έμ— νƒ€μž…
  • νƒ€μž…μ˜ 속성을 얻을 λ•ŒλŠ”, []λ₯Ό μ‚¬μš©ν•œλ‹€.

    • type.field둜 μ ‘κ·Όν•  수 μ—†μŒ

      Cannot access 'Person.first' because 'Person' is a type, but not a namespace. Did you mean to retrieve the type of the property 'first' in 'Person' with 'Person["first"]'? image

    • Person 클래슀λ₯Ό νƒ€μž…(type)으둜 μ‚¬μš©ν•  것이기 λ•Œλ¬Έμ—, λ„€μž„μŠ€νŽ˜μ΄μŠ€μ²˜λŸΌ 속성에 직접 μ ‘κ·Όν•  수 μ—†λ‹€.

      • 클래슀: νŠΉμ • 객체λ₯Ό μƒμ„±ν•˜κΈ° μœ„ν•œ blueprint
      • λ„€μž„μŠ€νŽ˜μ΄μŠ€: μ½”λ“œλ₯Ό λ…Όλ¦¬μ μœΌλ‘œ κ·Έλ£Ήν™”ν•˜κ³ , μ „μ—­ μŠ€μ½”ν”„μ˜ μ˜€μ—Όμ„ 방지 (λ„€μž„μŠ€νŽ˜μ΄μŠ€λ₯Ό 톡해 ν•¨μˆ˜, 클래슀, λ³€μˆ˜ 등에 μ ‘κ·Ό)
  • μΈλ±μŠ€μ—λŠ” union, κΈ°λ³Έν˜• (js νƒ€μž…)을 포함해 μ–΄λ–€ νƒ€μž…μ΄λ“  μ‚¬μš©ν•  수 μžˆλ‹€.

type PersonEl = Person["first" | "last"]; // union!
//   ^? type PersonEl = string
type Tuple = [string, number, Date]; // κΈ°λ³Έν˜•
type TupleEl = Tuple[number];
//   ^? type TupleEl = string | number | Date

Value vs Type πŸ€”

πŸ“Œ this

| | this | | --------- | ------------------- | | κ°’ value | μžλ°”μŠ€ν¬λ¦½νŠΈμ˜ this | | νƒ€μž… type | λ‹€ν˜•μ„± this |

  • value둜 μ“°μ΄λŠ” thisλŠ”? JS의 thisλ‹€.
  • type으둜 μ“°μ΄λŠ” thisλŠ”? λ‹€ν˜•μ„± this(polymorphic this)λ‹€.

    polymorphic thisλŠ”? μžλ°”μŠ€ν¬λ¦½νŠΈμ™€ λ§ˆμ°¬κ°€μ§€λ‘œ ν˜„μž¬ μ‹€ν–‰ μ»¨ν…μŠ€νŠΈλ₯Ό μ°Έμ‘°ν•©λ‹ˆλ‹€.
    μ—¬λŸ¬ ν΄λž˜μŠ€λ‚˜ κ°μ²΄μ—μ„œ κ³΅ν†΅λœ λ©”μ„œλ“œλ₯Ό μ •μ˜ν•˜λ©΄μ„œλ„ 각기 λ‹€λ₯Έ λ™μž‘μ„ κ΅¬ν˜„ν•  수 μžˆλŠ”λ°,
    주둜 상속과 μΈν„°νŽ˜μ΄μŠ€λ₯Ό 톡해 κ΅¬ν˜„λ©λ‹ˆλ‹€. μœ μ—°μ„±μ„ ν™œμš©!

πŸ“Œ polymorphic this

  • ν΄λž˜μŠ€μ™€ 상속, μ˜€λ²„λΌμ΄λ”©
class Animal {
  move(distance: number) {
    console.log(`${this.constructor.name} moved ${distance} meters.`);
    // thisλŠ” λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•œ 객체이닀
  }
}

class Dog extends Animal {
  move(distance: number) {
    console.log("Dog is running...");
    super.move(distance);
  }
}

class Bird extends Animal {
  move(distance: number) {
    console.log("Bird is flying...");
    super.move(distance);
  }
}

const dog = new Dog();
dog.move(10); // Dog is running... Dog moved 10 meters.

const bird = new Bird();
bird.move(20); // Bird is flying... Bird moved 20 meters.
  • μΈν„°νŽ˜μ΄μŠ€, λ©”μ„œλ“œ 체이닝
interface Movable {
  move(distance: number): this;
}

class Car implements Movable {
  move(distance: number): this {
    console.log(`Car drove ${distance} kilometers.`);
    return this;
  }

  refuel(amount: number): this {
    console.log(`Car refueled with ${amount} liters.`);
    return this;
  }
}

class Plane implements Movable {
  move(distance: number): this {
    console.log(`Plane flew ${distance} miles.`);
    return this;
  }

  maintain(): this {
    console.log("Plane underwent maintenance.");
    return this;
  }
}

const car = new Car();
const plane = new Plane();

car.move(100).refuel(50);
// Output:
// Car drove 100 kilometers.
// Car refueled with 50 liters.

plane.move(1000).maintain();
// Output:
// Plane flew 1000 miles.
// Plane underwent maintenance.

πŸ“Œ &와 |

| | & | | | | --------- | ------------ | ------ | | κ°’ value | AND | OR | | νƒ€μž… type | intersection | union |

πŸ“Œ const

| | const | | --------- | ------------------------------------------- | | κ°’ value | const μƒˆ λ³€μˆ˜ μ„ μ–Έ | | νƒ€μž… type | as const λ¦¬ν„°λŸ΄ ν‘œν˜„μ‹μ˜ μΆ”λ‘ λœ νƒ€μž…μ„ λ°”κΏˆ |

πŸ“Œ extends

  • μ„œλΈŒν΄λž˜μŠ€ λ˜λŠ” μ„œλΈŒνƒ€μž…, μ œλ„€λ¦­ νƒ€μž…μ˜ ν•œμ •μžλ₯Ό μ •μ˜ν•  수 μžˆλ‹€.
  • μ„œλΈŒν΄λž˜μŠ€
class Animal {
  move() {
    console.log("Moving along!");
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof! Woof!");
  }
}
  • μ„œλΈŒνƒ€μž…
interface Person {
  name: string;
  age: number;
}

interface Employee extends Person {
  employeeId: number;
}

const employee: Employee = {
  name: "John",
  age: 30,
  employeeId: 1234,
};
  • μ œλ„€λ¦­μ˜ ν•œμ •μž
function logLength<T extends { length: number }>(item: T): void {
  console.log(item.length);
}

logLength("Hello, TypeScript!"); // 18
logLength([1, 2, 3, 4, 5]); // 5

logLength(123); // Error: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.

πŸ“Œ in

  • loop λ˜λŠ” mapped νƒ€μž…μ— λ“±μž₯
loop
  • λ°˜λ³΅λ¬Έμ—μ„œ 객체의 속성을 μˆœνšŒν•  λ•Œ μ‚¬μš©λ©λ‹ˆλ‹€. 주둜 for...in 루프와 ν•¨κ»˜ μ‚¬μš©λ˜λ©°, 객체의 각 속성을 λ°˜λ³΅ν•˜λ©΄μ„œ μž‘μ—…μ„ μˆ˜ν–‰ν•©λ‹ˆλ‹€.

    const obj = { a: 1, b: 2, c: 3 };
    
    for (const key in obj) {
      console.log(`${key}: ${obj[key]}`);
    }
    // 좜λ ₯:
    // a: 1
    // b: 2
    // c: 3
    
mapped
  • Mapped νƒ€μž…μ—μ„œ in μ—°μ‚°μžλŠ” 쑰건뢀 νƒ€μž…κ³Ό ν•¨κ»˜ μ‚¬μš©λ  λ•Œ νƒ€μž… λ³€ν™˜μ„ μˆ˜ν–‰ν•˜λŠ” 데 μ‚¬μš©λ©λ‹ˆλ‹€ (객체의 λͺ¨λ“  속성을 λ°˜λ³΅ν•˜λ©΄μ„œ μƒˆλ‘œμš΄ νƒ€μž…μ„ λ§Œλ“€κ±°λ‚˜ μˆ˜μ •ν•˜λŠ” 데 μ‚¬μš©)

    type Optional<T> = {
      [K in keyof T]?: T[K];
      // in μ—°μ‚°μžλŠ” keyof Tλ₯Ό 톡해 Person의 λͺ¨λ“  속성을 반볡
      // 각 속성에 λŒ€ν•΄ ?:λ₯Ό μ‚¬μš©ν•˜μ—¬ μ˜΅μ…”λ„
    };
    
    interface Person {
      name: string;
      age: number;
    }
    
    type OptionalPerson = Optional<Person>;
    // { name?: string; age?: number; }
    

νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ 잘 λ™μž‘ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄?

  • type space νƒ€μž… 곡간과 value spaceλ₯Ό ν˜Όλ™ν•΄μ„œ 잘λͺ» μž‘μ„±ν–ˆμ„ κ°€λŠ₯성이 크닀.
function email({
  to: Person; // κ°’μ˜ 관점에 ν•΄μ„λ˜μ—ˆκΈ° λ•Œλ¬Έμ— μ—λŸ¬!
  subject: string;
  body: string;
}) {
  // ...
}

// κ°’κ³Ό νƒ€μž…μ„ κ΅¬λΆ„ν•˜λŠ” 이유
function email(
  {to, subject, body}: // κ°’
  {to: Person, subject: string, body: string} // νƒ€μž…
) {
  // ...
}

image

Things to Remember

  • Know how to tell whether you're in type space or value space while reading a TypeScript expression. Use the TypeScript playground to build an intuition for this.
    • νƒ€μž… 곡간과 κ°’ 곡간을 κ΅¬λΆ„ν•˜λŠ” 방법을 터득해야함
    • νƒ€μž…μŠ€ν¬λ¦½νŠΈ ν”Œλ ˆμ΄κ·ΈλΌμš΄λ“œ https://www.typescriptlang.org/play/?#code/
  • Every value has a static type, but this is only accessible in type space. Type space constructs such as type and interface are erased and are not accessible in value space.
    • λͺ¨λ“  값은 νƒ€μž…μ„ κ°€μ§€μ§€λ§Œ, 이 νƒ€μž…μ€ νƒ€μž… κ³΅κ°„μ—μ„œλ§Œ μ ‘κ·Όν•  수 μžˆλ‹€.
    • νƒ€μž… κ³΅κ°„μ—μ„œ λ§Œλ“  νƒ€μž…, μΈν„°νŽ˜μ΄μŠ€λŠ” κ°’ κ³΅κ°„μ—μ„œλŠ” μ‚­μ œλœλ‹€.
      • typeκ³Ό interfaceλŠ” type space에 쑴재
  • Some constructs, such as class or enum, introduce both a type and a value.
    • class와 enum은 νƒ€μž…κ³Ό κ°’μœΌλ‘œ λ‘˜ λ‹€ μ‚¬μš©λ  수 μžˆλ‹€
  • typeof, this, and many other operators and keywords have different meanings in type space and value space.
    • typeof, this λ“±μ˜ λ§Žμ€ μ—°μ‚°μžμ™€ ν‚€μ›Œλ“œλŠ” => type space와 value spaceμ—μ„œ λ‹€λ₯Έ λͺ©μ μœΌλ‘œ μ‚¬μš©λ  수 μžˆλ‹€

μ•„μ΄ν…œ 9 νƒ€μž… λ‹¨μ–Έλ³΄λ‹€λŠ” νƒ€μž… 선언을 μ‚¬μš©ν•˜κΈ° Prefer Type Annotations to Type Assertions

  • μ™œ? νƒ€μž… 단언은 νƒ€μž… μ²΄μ»€μ—κ²Œ 였λ₯˜λ₯Ό λ¬΄μ‹œν•˜λΌκ³  ν•˜λŠ” 것이기 λ•Œλ¬Έμ΄λ‹€
interface Person {
  name: string;
}

// Type Annotations은 ν• λ‹Ήλœ 값이 ν•΄λ‹Ή μΈν„°νŽ˜μ΄μŠ€λ₯Ό λ§Œμ‘±ν•˜λŠ”μ§€
const alice: Person = { name: "Alice" };
//    ^? const alice: Person

// Type AssertionsλŠ” μΆ”λ‘ ν•œ νƒ€μž…μ΄ μžˆλ”λΌλ„, ν•΄λ‹Ή νƒ€μž…μœΌλ‘œ κ°„μ£Όν•œλ‹€
const bob = { name: "Bob" } as Person; // Type Assertions
//    ^? const bob: Person

μž‰μ—¬ 속성 체크 excess property checking도 μ μš©λ˜μ§€ μ•ŠμŒ

const alice: Person = {
  name: "Alice",
  occupation: "TypeScript developer",
  // ~~~~~~~~~ Object literal may only specify known properties,
  //           and 'occupation' does not exist in type 'Person'
};
const bob = {
  name: "Bob",
  occupation: "JavaScript developer",
} as Person; // μ—λŸ¬κ°€ μ—†μŒ!

이전에 νƒ€μž… 단언을 μ‚¬μš©ν•˜λ˜ μ½”λ“œ ν˜•νƒœ

const bob = <Person>{};
  • ν˜„μž¬λŠ” tsx의 μ»΄ν¬λ„ŒνŠΈ νƒœκ·Έλ‘œ μΈμ‹λ˜κΈ° λ•Œλ¬Έμ— 쓰이지 μ•ŠλŠ”λ‹€. image

ν™”μ‚΄ν‘œ ν•¨μˆ˜

  • μ΅œμ’…μ μœΌλ‘œ μ›ν•˜λŠ” νƒ€μž…μ„ 직접 λͺ…μ‹œν•˜κ³ , νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ ν• λ‹Ήλ¬Έμ˜ μœ νš¨μ„±μ„ κ²€μ‚¬ν•˜λ„λ‘ 함
  • 단, ν•¨μˆ˜ 호좜 체이닝이 μ—°μ†λ˜λŠ” 곳은, 체이닝 μ‹œμž‘λΆ€ν„° λͺ…λͺ…λœ νƒ€μž…μ„ κ°€μ Έμ•Ό ν•œλ‹€ (μ •ν™•ν•œ 곳에 였λ₯˜ ν‘œκΈ°ν•˜κΈ° μœ„ν•¨)
interface Person {
  name: string;
}
// νƒ€μž… 단언
const people = ["alice", "bob", "jan"].map((name) => ({ name } as Person)); // Type is Person[]

// 단언 μ‹œ λŸ°νƒ€μž„ 였λ₯˜ λ°œμƒ
const wrongPeople = ["alice", "bob", "jan"].map((name) => ({} as Person)); // Type is Person[]

// μ΅œμ’…μ μœΌλ‘œ μ›ν•˜λŠ” νƒ€μž…μ„ 직접 λͺ…μ‹œν•˜κ³ , νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ ν• λ‹Ήλ¬Έμ˜ μœ νš¨μ„±μ„ κ²€μ‚¬ν•˜λ„λ‘ 함
const goodPeople: Person[] = ["alice", "bob", "jan"].map((name) => ({ name })); // OK

Type Assertion이 ν•„μš”ν•œ 경우

  • νƒ€μž… 체컀가 μΆ”λ‘ ν•œ νƒ€μž…λ³΄λ‹€, κ°œλ°œμžκ°€ νŒλ‹¨ν•œ νƒ€μž…μ΄ 더 μ •ν™•ν•  λ•Œ
    • ex) DOM element
document.querySelector("#myButton")?.addEventListener("click", (e) => {
  e.currentTarget;
  // ^? (property) Event.currentTarget: EventTarget | null
  // currentTarget is #myButton is a button element: νƒ€μž…μŠ€ν¬λ¦½νŠΈκ°€ μ•Œμ§€ λͺ»ν•˜λŠ” 정보, why? DOM에 μ ‘κ·Όν•  수 μ—†κΈ° λ•Œλ¬Έμ—
  const button = e.currentTarget as HTMLButtonElement;
  //    ^? const button: HTMLButtonElement
});

nonNullAssertion: !

  • λ³€μˆ˜μ˜ μ ‘λ‘μ‚¬λ‘œ 쓰인 !λŠ”? boolean의 λΆ€μ •λ¬Έ
  • μ ‘λ―Έμ‚¬λ‘œ 쓰인 !λŠ”? κ·Έ 값이 Null이 μ•„λ‹ˆλΌλŠ” λ‹¨μ–Έλ¬ΈμœΌλ‘œ ν•΄μ„λœλ‹€.
  • 주의
    • 단언문은 컴파일 κ³Όμ •μ—μ„œ μ œκ±°λœλ‹€.
    • νƒ€μž… μ²΄μ»€λŠ” μ•Œμ§€ λͺ»ν•˜μ§€λ§Œ, 값이 null이 μ•„λ‹˜μ„ ν™•μ‹ ν•  수 μžˆμ„ λ•Œ μ‚¬μš©ν•œλ‹€
      • ν™•μ‹ ν•  수 μ—†λ‹€λ©΄ 쑰건문으둜 null 체크 ν•΄μ•Όν•œλ‹€.

νƒ€μž… 단언을 μ‚¬μš©ν•œ νƒ€μž… λ³€ν™˜κ³Ό unknown

interface Person {
  name: string;
}
const body = document.body;
const el = body as Person;
//         ~~~~~~~~~~~~~~
// Conversion of type 'HTMLElement' to type 'Person' may be a mistake because
// neither type sufficiently overlaps with the other. If this was intentional,
// convert the expression to 'unknown' first.

image

  • μ„œλ‘œμ˜ μ„œλΈŒ νƒ€μž…μ΄ μ•„λ‹ˆκΈ° λ•Œλ¬Έμ— λ³€ν™˜μ΄ λΆˆκ°€λŠ₯ν•˜λ‹€.

    • λ§Œμ•½ unknown νƒ€μž…μ„ μ‚¬μš©ν•΄ μž„μ˜μ˜ νƒ€μž… κ°„μ˜ λ³€ν™˜μ΄ κ°€λŠ₯ν•œ, 항상 λ™μž‘ν•˜λŠ” 단언을 μ‚¬μš©ν•˜λŠ” 경우? 무언가 μœ„ν—˜ν•œ λ™μž‘μ„ ν•˜κ³  μžˆμŒμ„ μ•Œ 수 있음

    • λͺ¨λ“  νƒ€μž…μ€ unknown의 μ„œλΈŒνƒ€μž…μ΄κΈ° λ•Œλ¬Έμ—, unknown 단언은 항상 λ™μž‘ν•œλ‹€

      const el = document.body as unknown as Person; // OK
      

Things to Remember

  • Prefer type annotations (: Type) to type assertions (as Type).
    • 단언보닀 선언을 μ‚¬μš©ν•˜μž
  • Know how to annotate the return type of an arrow function.
    • ν™”μ‚΄ν‘œ ν•¨μˆ˜μ˜ λ°˜ν™˜ νƒ€μž… λͺ…μ‹œν•˜λŠ” 법 μ•ŒκΈ°
  • Use type assertions and non-null assertions only when you know something about types that TypeScript does not.
    • νƒ€μž…μŠ€ν¬λ¦½νŠΈλ³΄λ‹€ 더 잘 μ•Œκ³  μžˆλŠ” 게 ν™•μ‹€ν•œ μƒν™©μ—μ„œλ§Œ νƒ€μž… 단언과 non-null 단언을 μ“°μž.
  • When you use a type assertion, include a comment explaining why it's valid. // 쒋은 ν˜‘μ—… μŠ΅κ΄€

// English
mnemonic [mneΒ·monΒ·ic] 연상 기호
intuition [inΒ·tuΒ·iΒ·tion] the ability to understand something immediately, without the need for conscious reasoning.
exclamation mark [exΒ·claΒ·maΒ·tion mark] !