4장 타입 설계

ClassBinu·2024년 4월 28일

28 유효한 상태만 표현하는 타입 지향

상태가 섞여있는 예

interface State {
  pageText: string;
  isLoading: boolean;
  error?: string;
}

이 구조의 문제는 각 필드가 독립적으로 관리되기 때문에 코드에서 예상치 못한 상태 조합이 발생할 수 있다는 것이다. 예를 들어, isLoading이 true일 때 pageText나 error가 설정되어 있어서는 안 되지만, 이를 강제할 수 있는 방법이 없다.

다소 길지만 상태를 구분한 바람직한 예

interface RequestsPending {
  state: 'pending';
}

interface RequestError {
  state: 'error';
  error: string;
}

interface RequestSuccess {
  state: 'ok';
  pageText: string;
}
type RequestState = RequestPending | RequestError | RequestSuccess;

interface State {
  currentPage: string;
  requests: {[page: string]: RequestState};
}

타입을 설계할 때는 어떤 값들을 포함하고 어떤 값들을 제외할지 신중하게 생각해야 한다. 유효한 상태를 표현하는 값만 허용한다면 코드를 작성하기 쉬워지고 타입 체크가 용이해 진다.

유효한 상태만 표현하는 타입을 지향해야 한다. 코드가 길어지거나 표현하기 어렵지만 결국은 시간을 절약하고 고통을 줄일 수 있다.

29 사용은 너그럽게, 생성은 엄격하게

함수 시그니처에서 매개변수 타입은 범위가 넓어도 되미나, 결과 반환은 구체적이어야 함.

30 문서에 타입 정보 쓰지 않기

31 타입 주변에 null 값 배치

strictNullChecks 설정

false: 어떤 타입 변수에도 null, undefined 할당 가능
true: any또는 각각의 타입에만 할당 가능

클래스를 만들 때는 필요한 모든 값이 준비되었을 때 생성하여 null이 존재하지 않도록 하는 것이 좋다.

32 유니온의 인터페이스보다 인터페이스의 유니온 사용하기

잘못된 예시: 유니온의 인터페이스

interface Layer {
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint | PointPaint;
}

이 경우에 레이아웃과 페인트가 서로 맞지 않는 타입이 존재한다.
차라리 분리해라.

개선 예시: 인터페이스의 유니온

interface FillLayer {
  layout: FillLayout;
  paint: FillPaint;
}

interface LineLayer {
  layout: LineLayout;
  paint: LinePaint;
}

interface PointLayer {
  layout: PointLayout;
  paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;

33 string 타입보다 더 구체적인 타입 사용하기

유니온으로 구체적인 값을 타입으로 지정
(enum은 추천하지 않는다.)

객체 속성 이름을 함수 매개변수로 받을 때는 string보다는 keyof T를 사용하기

interface Person {
    name: string;
    age: number;
    hasPet: boolean;
}

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const person: Person = {
    name: 'John Doe',
    age: 30,
    hasPet: false,
};

const name: string = getProperty(person, 'name');
const age: number = getProperty(person, 'age');
const hasPet: boolean = getProperty(person, 'hasPet');
// const notExist = getProperty(person, 'notExist'); // 컴파일 에러: 'notExist'가 'Person' 타입에 존재하지 않습니다.

extends: 타입스크립트에서 타입 제한을 두는 키워드입니다. 이는 K가 갖을 수 있는 타입을 keyof T의 결과로 제한합니다.
keyof T: 타입 T의 모든 속성 이름을 문자열 또는 숫자 리터럴의 유니온(합집합) 타입으로 추출합니다. 예를 들어, T가 { name: string; age: number; } 인터페이스라면, keyof T의 결과는 "name" | "age"가 됩니다.

34 부정확한 타입보다 미완성 타입

이상한 타입 쓰느니 any를 쓴다.
하지만 이건 어디까지나 임시방편임.

타입이 구체적이라고해서 정확도가 무조건 올라가는 건 아님. 정확한 타입을 써야 함.

any: 타입 검사 비활성화. 어떠한 연산도 수행 가능.
unknown: 아무런 연산 수행 불가. 명시적인 타입 검사 또는 타입 단언이 필요하다.

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

let notSure2: unknown = 4;
notSure2 = "maybe a string instead";
notSure2 = false; // okay, definitely a boolean

// 'any' 타입은 아래처럼 모든 연산이 가능
console.log(notSure.ifItExists()); // okay, ifItExists might exist at runtime
console.log(notSure.toFixed());    // okay, toFixed exists (but the compiler doesn't check)

// 'unknown' 타입은 추가적인 타입 확인이 필요
if (typeof notSure2 === 'string') {
    console.log(notSure2.toUpperCase()); // okay, now we know it is a string
}
if (typeof notSure2 === 'number') {
    console.log(notSure2.toFixed()); // okay, now we know it is a number
}

unkown은 타입 단언이든 타입 추론이든 어쨌든 타입이 추론되어야 함.

any는 '다 가능!' 느낌이고 unkown은 '다 안 됨!' 느낌임.

35 데이터가 아닌 API 명세보고 타입 만들기

데이터에 드러나지 않는 예외적인 경우가 문제를 야기한다.
데이터보다는 명세로부터 코드를 생성해라.

36 해당 분야 용어로 타입 이름 짓기

컴퓨터 과학에서 어려운 일은 단 두 가지뿐이다. 캐시 무효화와 이름 짓기. - 필 칼튼

이름 짓기 3원칙

  • 동일한 의미를 나타낼 떄는 같은 용어 사용(동의어가 아닌 동일어)
  • data, info, thing, item, object, entity 같은 모호하고 의미 없는 이름 피하기(해당 분야에서 특별한 의미라면 제외)
  • 포함된 내용이나 계산 방식이 아닌 데이터 자체가 무엇인지 고려
    INodeList보다 Directory가 더 의미 있음

37 공식 명칭에는 상표 붙이기

interface Vector2D {
  _brand: '2d';
  x: number;
  y: number;
}
function vec2D(x: number, y: number): Vector2D {
  return {x, y, _brand: '2d'};
}
function calculateNorm(p: Vector2D) {
  return Math.sqrt(p.x * p.x + p.y * p.y);
}

calculateNorm(vec2D(3, 4)); // 5 반환
const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D): // ~~~~ _brand 속성이 .. 형식에 없습니다.

덕 타이핑

사람이 오리처럼 행동하면 오리로 봐도 무방하다
타입을 미리 정하는게 아니라 실행 되었을 때 타입을 정한다.

타입스크립트는 구조적 타이핑(덕 타이핑)을 사용함. 그래서 값을 세밀하게 구본하지 못하는 경우가 있음.
즉, 객체의 구조가 같다면, 같은 타입으로 간주합니다. 이는 때로는 원치 않는 타입 호환성 문제를 야기할 수 있습니다.

객체 예시

객체가 특정 인퍼페이스 또는 타입에 필요한 모든 필드와 메서드를 가지고 있으면 추가적인 필드가 있더라도 해당 인터페이스 또는 타입으로 간주됨.

interface Person {
    name: string;
    age: number;
}

let employee: Person = {
    name: "John",
    age: 30,
    department: "Finance"  // 이 추가 필드는 타입 검사에 영향을 주지 않습니다.
};

이 경우 3d 객체를 2d 타입으로 호환 가능하다고 판단해 버림.
이때 상표 기법을 활용 가능.

interface Person {
    _brand: "Person";
    name: string;
    age: number;
}

function createPerson(name: string, age: number): Person {
    return {
        _brand: "Person",
        name: name,
        age: age
    };
}

let employee = createPerson("John", 30);  // employee는 Person 타입입니다.

// 이제 employee 객체는 상표 필드 _brand를 갖고 있으므로, Person 인터페이스를 충실히 구현한 것으로 간주됩니다.
// department 필드를 추가하려면 인터페이스를 수정하거나 확장해야 합니다.

// 추가 필드 처리
interface Employee extends Person {
    _brand: "Employee";
    department: string;
}

function createEmployee(name: string, age: number, department: string): Employee {
    return {
        _brand: "Employee",
        name: name,
        age: age,
        department: department
    };
}

let employee: Employee = createEmployee("John", 30, "Finance");

0개의 댓글