이번에는 반드시 타입스크립트를 🌟잘🌟 써보자!

boyeonJ·2023년 5월 10일
0

TypeScript

목록 보기
1/12
post-thumbnail

1. 타입을 집합의 개념으로 이해하기

타입은 값의 집합으로 이해될 수 있습니다. 모든 타입은 값을 포함하는 집합이며, 일부는 무한하고, 일부는 유한합니다. unknown은 모든 값을 포함하는 범용 집합이고, never는 아무 값을 포함하지 않는 빈 집합입니다.

타입 간의 관계는 집합 간의 관계로 해석할 수 있습니다. & 연산자는 인터섹션(교집합)을 만들며, | 연산자는 유니온(합집합)을 만듭니다. 인터섹션은 더 작은 집합을 나타내며, 공통적으로 사용 가능한 필드를 포함합니다. 유니온은 더 큰 집합을 나타내며, 잠재적으로 공통적으로 사용 가능한 필드가 더 적습니다.

이러한 집합의 개념은 할당 가능성을 이해하는 데 도움이 됩니다. 할당은 값의 타입이 대상 타입의 하위 집합인 경우에만 허용됩니다. 집합의 개념을 사용하면 타입 시스템의 동작을 이해하기 쉽게 할 수 있습니다.

인터섹션과 유니온

& 연산자는 인터섹션(교집합)을 만듭니다. 이는 두 개 이상의 타입을 결합하여 새로운 타입을 생성합니다. 즉, 인터섹션 타입은 각 타입이 가지고 있는 모든 속성을 포함하는 타입이 됩니다.

type Person = {
  name: string;
  age: number;
};

type Employee = {
  company: string;
  position: string;
};

type EmployeeWithPerson = Employee & Person;

const employee: EmployeeWithPerson = {
  name: "John",
  age: 30,
  company: "ABC Corp",
  position: "Manager",
};

console.log(employee);
// 출력: { name: "John", age: 30, company: "ABC Corp", position: "Manager" }

반면, | 연산자는 유니온(합집합)을 만듭니다. 이는 두 개 이상의 타입 중 하나를 선택할 수 있는 타입을 생성합니다. 유니온은 더 큰 집합을 나타내며, 각 타입의 공통적인 필드만을 포함합니다. 즉, 유니온 타입은 여러 타입 중 하나의 속성만을 가질 수 있는 타입입니다.

type Status = "Pending" | "InProgress" | "Completed";

function getStatusMessage(status: Status): string {
  switch (status) {
    case "Pending":
      return "Task is pending.";
    case "InProgress":
      return "Task is in progress.";
    case "Completed":
      return "Task has been completed.";
    default:
      return "Invalid status.";
  }
}

2. 선언된 타입과 좁혀진(narrowed) 타입의 이해

타입스크립트의 매우 강력한 특징 중 하나는 제어 흐름에 따라 자동으로 타입을 좁히는 것입니다. 이것은 변수가 코드 위치의 특정 지점에서 연관된 두 가지 타입, 즉 선언 타입과 좁혀진 타입을 가짐을 의미합니다.

function foo(x: string | number) {
  if (typeof x === 'string') {
    // x'의 타입은 string타입으로 좁혀졌습니다. 따라서 .length가 가능합니다.
    console.log(x.length);

    // 할당을 하게되면 좁혀진 타입이 아닌 선언한 타입이 됩니다.
    x = 1;
    console.log(x.length); // x는 지금 number 타입이므로 불가능합니다.
    } else {
        ...
    }
}

3. 옵셔널 필드 대신에 구분된 유니온 사용

옵셔널 필드

옵셔널 필드는 타입스크립트에서 변수 또는 객체의 필드에 대해 값이 있을 수도 있고 없을 수도 있는 경우를 나타냅니다. 이는 해당 필드의 값이 선택적이며, 반드시 존재하지 않아도 된다는 의미입니다.

옵셔널 필드는 필드 이름 뒤에 물음표 ?를 붙여서 표시됩니다. 이를 통해 변수나 객체의 필드가 선택적인지 명시적으로 표시할 수 있습니다.

type Person = {
  name: string;
  age?: number;
};

단언(assertion)

! 기호는 타입스크립트에서 "확신한다"는 의미로 사용되는 Non-null assertion 연산자입니다. 변수 뒤에 !를 붙이면 해당 변수가 null 또는 undefined가 아님을 확신하는 것을 나타냅니다. 이를 통해 컴파일러에게 해당 변수는 Nullable하지 않다는 것을 알려줍니다.

하지만 Non-null assertion 연산자는 주의해서 사용해야 합니다. 만약 해당 변수가 실제로 null 또는 undefined인 상태에서 Non-null assertion 연산자를 사용하면 런타임 에러가 발생(Cannot read property 'propertyName' of null)할 수 있습니다. 따라서 변수를 사용하기 전에 항상 값이 존재하는지 확인하는 것이 좋습니다

옵셔널 필드 대신에 구분된 유니온 사용

옵셔널 필드를 사용할 경우 단언이 필요할 경우가 있는데, 이 보다는 유니온을 사용하여 구분하는것이 더욱 좋습니다.

옵셔널 필드를 사용할 경우, 해당 필드가 존재하지 않을 수 있음을 나타냅니다. 따라서 해당 필드에 접근할 때에는 null 체크를 통해 안전하게 접근해야 합니다. 유니온 타입을 사용하여 필드를 구분하는 것은 이러한 안전성을 보장하면서도 명시적으로 필드의 존재 여부를 표현할 수 있는 방법 중 하나입니다.

옵셔널 필드를 단언(!)하여 Non-null로 강제 변환하는 것은 컴파일러에게 "이 필드는 절대로 null이 아닐 것이다"라고 알려주는 것입니다. 그러나 이는 개발자가 해당 필드에 대한 책임을 갖게 되므로 주의가 필요합니다. 만약 옵셔널 필드에 실제로 null이 들어갈 가능성이 있는 상황에서 단언을 사용하면, 런타임 에러가 발생할 수 있습니다.

type Shape = {
  kind: 'circle' | 'rect';
  radius?: number;
  width?: number;
  height?: number;
}

function getArea(shape: Shape) {
  return shape.kind === 'circle' ? 
    Math.PI * shape.radius! ** 2
    : shape.width! * shape.height!;
}

위 코드 대신 아래의 코드를 사용하는 것이 더욱 안전하다.

type Circle = { kind: 'circle'; radius: number };
type Rect = { kind: 'rect'; width: number; height: number };
type Shape = Circle | Rect;

function getArea(shape: Shape) {
    return shape.kind === 'circle' ? 
        Math.PI * shape.radius ** 2
        : shape.width * shape.height;
}

4. 타입 단언을 피하기 위한 타입 명제 사용

명시적 타입 단언(타입 명제)

타입 명제(Type Assertion)는 타입스크립트에서 특정 값에 대해 개발자가 해당 값의 타입을 컴파일러에게 명시적으로 알려주는 것을 의미합니다. 타입스크립트는 정적 타입 언어이며, 컴파일러는 변수와 표현식의 타입을 추론하려고 합니다. 그러나 때로는 컴파일러가 타입을 정확하게 추론하지 못하는 경우가 발생할 수 있습니다. 이럴때 개발자가 명시적 타입 단언을 통해 컴파일러에게 미리 알려줄 수 있습니다.

명시적 타입 단언(Explicit Type Assertion)은 TypeScript에서 변수나 표현식의 타입을 개발자가 직접 명시적으로 지정하는 것을 말합니다. 이를 통해 TypeScript 컴파일러에게 "나는 이 변수나 표현식이 지정한 타입을 가지고 있다고 확신한다"라고 알려줄 수 있습니다.

명시적 타입 단언은 다음과 같은 두 가지 문법을 사용할 수 있습니다:

  • <타입>값: 태그 구문 (Tagged Syntax)
  • 값 as 타입: as 구문 (As Syntax)
let value: any = 'hello';
let length: number = (<string>value).length; // 태그 구문 사용
let uppercase: string = (value as string).toUpperCase(); // as 구문 사용

명시적 타입 단언은 컴파일러에게 개발자의 의도를 알리는 역할을 합니다. 그러나 주의해야 할 점은 잘못된 타입 단언은 런타임 에러를 발생시킬 수 있다는 것입니다. 따라서 타입 단언을 사용할 때에는 해당 값이 실제로 지정한 타입을 가지고 있는지 확인하고 신중하게 사용해야 합니다.

타입 가드(Type Guard) 문법

타입 가드는 TypeScript에서 특정 조건을 확인하여 변수의 타입을 좁히는 역할을 수행하는 패턴입니다. 타입 가드는 일반적으로 함수의 형태로 작성되며, is 키워드를 사용하여 특정 조건을 판별합니다.

타입 가드를 사용하면 더 구체적인 타입으로 변수의 타입을 추론할 수 있으며, 이는 코드의 가독성과 안정성을 높일 수 있습니다. 일반적으로, 타입 가드는 함수나 조건문을 사용하여 변수의 타입을 판별합니다. TypeScript는 이러한 판별식을 인식하고 변수의 타입을 좁힐 수 있도록 합니다.

가장 일반적인 형태의 타입 가드는 타입 검사 후 instanceof나 타입을 검사하는 연산자(typeof, in, key in obj 등)를 사용하는 것입니다.

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function isNumber(value: unknown): value is number {
  return typeof value === 'number';
}

function isObject(value: unknown): value is object {
  return typeof value === 'object' && value !== null;
}

// 사용 예시
function printValue(value: unknown) {
  if (isString(value)) {
    console.log(value.toUpperCase()); // value는 string 타입으로 추론됩니다.
  } else if (isNumber(value)) {
    console.log(value.toFixed(2)); // value는 number 타입으로 추론됩니다.
  } else if (isObject(value)) {
    console.log(Object.keys(value)); // value는 object 타입으로 추론됩니다.
  } else {
    console.log('Unknown type');
  }
}

타입 단언을 피하기 위한 타입 명제 사용를 사용했을떄의 이점

  1. 정확한 타입 추론: 타입 명제를 사용하면 타입 가드 함수가 조건을 충족하는 경우 변수의 타입이 좁혀집니다. 이로써 타입스크립트 컴파일러는 해당 변수를 사용하는 곳에서 정확한 타입을 추론할 수 있습니다. 따라서 명시적 타입 단언을 사용하지 않아도 올바른 타입 추론이 이루어집니다.

  2. 정적 타입 검사: 타입 명제를 사용하여 타입 가드를 지정하면 타입스크립트 컴파일러가 해당 변수의 타입을 정확하게 알고, 이에 따라 정적 타입 검사를 수행할 수 있습니다. 이는 코드의 안정성을 향상시키고, 잠재적인 타입 관련 오류를 사전에 감지할 수 있는 장점을 제공합니다.

  3. 유지 보수 용이성: 명시적 타입 단언을 사용하는 경우, 코드가 복잡해지고 변경이 발생할 때 타입 단언도 함께 수정해야 할 수 있습니다. 하지만 타입 명제를 사용하여 타입 가드를 지정하면 타입 단언과는 달리 타입 가드 함수 자체만 수정하면 됩니다. 이는 코드 유지 보수를 더욱 용이하게 만들어줍니다.

  4. 가독성과 명확성: 타입 명제를 사용하여 타입 가드를 지정하면 코드의 의도가 명확하게 드러나고 가독성이 향상됩니다. 다른 개발자가 코드를 이해하고 유지 보수하는 데 도움이 됩니다.

따라서 타입 명제를 사용하여 타입 가드를 지정하는 것은 명시적 타입 단언을 사용하는 것보다 코드의 안정성과 가독성을 개선하는 데 도움이 됩니다. 따라서 아래의 첫번째 코드보다 두번째 코드를 사용하는것이 더욱 좋습니다.

type Circle = { kind: 'circle'; radius: number };
type Rect = { kind: 'rect'; width: number; height: number };
type Shape = Circle | Rect;

function isCircle(shape: Shape) {
  return shape.kind === 'circle';
}

function isRect(shape: Shape) {
  return shape.kind === 'rect';
}

const myShapes: Shape[] = getShapes();

// 타입스크립트가 필터링 된 것을 모르기 때문에 에러가 발생합니다.
// 타입 좁히기
const circles: Circle[] = myShapes.filter(isCircle);

// 다음과 같은 단언을 추가하고 싶을 수 있습니다.
// const circles = myShapes.filter(isCircle) as Circle[];
function isCircle(shape: Shape): shape is Circle {
    return shape.kind === 'circle';
}

function isRect(shape: Shape): shape is Rect {
    return shape.kind === 'rect';
}

...
// 이제 Circle[] 타입을 올바르게 유추합니다.
const circles = myShapes.filter(isCircle);

https://velog.io/@lky5697/11-tips-that-help-you-become-a-better-typescript-programmer

0개의 댓글