TS스터디 이펙티브 item19~21

온호성·2023년 4월 10일
0

😶item 19 추론 가능한 타입을 사용해 장황한 코드 방지하기

ts의 많은 타입 구문은 사실 불필요하다. 코드의 모든 변수에 타입을 선언하는 것은 비생산적이며 형편없는 스타일이다.

예를 들어서 const a:number = 235와 같이 이미 235이란 숫자를 통해 타입을 알 수 있는데, 굳이 number를 명시할 필요는 없다.

ts 입력을 받아 연산하는 함수가 어떤 타입을 반환하는지 정확히 알고있다. 따라서 반환 타입을 작성하지 않아도 되는 경우가 있다.

function square(nums: number[]){
 return nums.map(x => x * x); 
}
const squares = square([1, 2, 3, 4]); // number[]

어디에 타입 구문을 넣고, 어디를 빼야할지 아는 것이 중요한 것 같다.

정보가 부족해서 타입스크립트가 스스로 타입을 판단하기 어려운 상황도 존재한다. 이럴 때 명시적 타입 구문이 필요하다. 함수의 매개변수가 그러하다.

타입 명시하면 좋은 경우

  • 객체 리터럴을 정의할 때 이미 타입이 추론되지만 타입을 명시하면 잉여 속성 체크가 작동하고 이는 오타 같은 오류를 잡는데 좋다 또한 변수가 사용하는 시점이 아니라 할당하는 시점에 표시되기 때문에도 좋다.
  • 이외에도 함수의 반환에도 타입 명시하여 오류 방지가 가능한데 함수에 대해 더욱 명확하게 알 수 있다 또한 명명된 타입을 사용함으로서 직관적인 표현이 된다

ts는 정적 타입 검사 기능을 제공하는 언어로, 코드에서 변수, 함수, 클래스 등에 타입을 명시하여 해당 값이 어떤 형식을 가져야 하는지 명확하게 정의할 수 있다. 이를 통해 코드의 가독성과 유지 보수성을 높일 수 있는 것이다.

하지만 타입을 명시하지 않으면 타입스크립트는 해당 값이 어떤 형식을 가져야 하는지 추론한다. 이는 간단한 코드에서는 유용하지만 복잡한 코드에서는 추론이 제대로 이루어지지 않을 가능성이 높다. 따라서 추론 가능한 타입을 사용하여 코드를 작성하면 장황한 코드를 방지할 수 있다.

예를 들어

function add(x, y) {
  return x + y;
}

const result = add(1, "2");

위 코드에서 add 함수의 인수 x와 y는 어떤 타입인지 명시되어 있지 않다. 이 경우 타입스크립트는 x와 y의 타입을 추론하게 되는데, 이 경우 x가 number 타입이고 y가 string 타입으로 추론된다. 그리고 이 경우 result 변수에는 12가 할당된다.

하지만 만약 add 함수를 다음과 같이 작성한다면,

function add(x: number, y: number) {
  return x + y;
}

const result = add(1, "2");

add 함수의 인수 x와 y에 number 타입을 명시해주면, 타입스크립트는 add 함수의 인수가 올바른 형식인지 런타임 이전에 검사하여 오류를 방지할 수 있다. 또한 result 변수에 "12"가 할당되는 오류도 방지할 수 있다.

따라서 추론 가능한 타입을 사용하면 코드의 가독성을 높이고, 장황한 코드를 방지할 수 있으며, 런타임 에러를 사전에 방지할 수 있다.

-요약-

  • 일반적으로 타입 추론이 되는 경우에는 명시적으로 타입을 작성하지 말것
  • 함수의 경우에는 함수 시그니처 타입을 사용하는 것이 좋다
  • 정해진 타입이 있는 특별한 경우에 명시적으로 타입을 정하자

🥵item 20 다른 타입에는 다른 변수 사용하기

ts에서는 다른 타입에 따라 다른 변수를 사용할 수 있다. 이를 통해 코드의 가독성과 유지 보수성을 높일 수 있는데 변수의 값은 바뀔 수 있지만 그 타입은 바뀌지 않아야 한다.

let id = "12-34-56"
fetchProduct(id)
id =123456 // 'number' 형식은 'string' 형식에 할당할 수 없습니다.
fetchProductBySerialNumber(id)

id 타입은 string 인데 number 값을 할당할 수 없다.
차라리 별도의 변수를 도입하여 다음처럼 작성하는 것이 좋다.

const id = "12-34-56"
fetchProduct(id)
const serial =123456 // 정상
fetchProductBySerialNumber(id)

별도의 변수를 사용하는게 바람직한 이유

  • 서로 관련이 없는 두 개의 값을 분리한다.
  • 변수명을 더 구체적으로 지을 수 있다.
  • 타입 추론을 향상시키며, 타입 구문이 불필요해진다.
  • 타입이 더 간결해진다. (string|number 대신 string과 number 사용)
  • let대신 const로 변수를 선언하게 된다. const로 변수를 선언하면 코드가 간결해지고 타입체커가 타입을 추론하기에도 좋다.

예를 들어서

function calculateArea(shape, measurement) {
  if (shape === "square") {
    return measurement * measurement;
  } else if (shape === "circle") {
    return Math.PI * measurement * measurement;
  }
}

위 코드에서 calculateArea 함수는 도형의 종류와 측정값을 인수로 받아 해당 도형의 면적을 계산하여 반환한다. 그러나 인수 shape와 measurement는 모두 any 타입으로 명시되어 있기 때문에, 코드를 읽는 사람은 shape와 measurement가 어떤 타입을 가져야 하는지 알기 어렵다.

또한 함수 본문에서 shape 값이 "square"인 경우에는 measurement의 제곱값을 반환하고, "circle"인 경우에는 원의 면적을 반환하는데, 이 두 가지 도형의 면적을 구하는 공식이 서로 다르기 때문에, 코드를 읽는 사람이 이를 파악하기 어렵다.

이러한 문제를 해결하기 위해 위에서 얘기한 대로 인수 shape와 measurement에 대해 각각 다른 타입을 사용해주면 되는데. 예를 들어, 다음과 같이 Shape와 Measurement이라는 인터페이스를 만들어서 각 도형의 형태와 측정값의 타입을 정의해줄 수 있다.

interface Shape {
  shapeType: "square" | "circle";
  measurement: number;
}

interface Square extends Shape {
  shapeType: "square";
  sideLength: number;
}

interface Circle extends Shape {
  shapeType: "circle";
  radius: number;
}

위 코드에서 Shape 인터페이스는 모든 도형의 공통 속성을 정의하고, Square와 Circle 인터페이스는 각각 사각형과 원의 고유한 속성을 정의한다.

이제 calculateArea 함수를 다음과 같이 작성하면 된다.

function calculateArea(shape: Shape) {
  if (shape.shapeType === "square") {
    const square = shape as Square;
    return square.sideLength * square.sideLength;
  } else if (shape.shapeType === "circle") {
    const circle = shape as Circle;
    return Math.PI * circle.radius * circle.radius;
  }
}

위 코드에서 Shape 인터페이스를 인수 타입으로 명시하고, shapeType 속성을 통해 어떤 도형인지 구분한다. 그리고 각 도형의 고유한 속성을 사용하기 위해 as 연산자를 사용한다.

-요약-

  • 변수는 바뀔 수 있지만, 타입은 일반적으로 바뀌지 않는다.
  • 타입이 다른 값을 이용할 때는 같은 변수를 사용하기 보다는 각각의 변수에 다른 타입을 적용해서 사용하는 것이 좋다.

🤥item 21 타입 넓히기

상수를 사용해서 변수를 초기화 할 때 타입을 명시하지 않으면 타입체커는 타입을 결정해야 한다. 이 말은 지정된 단일 값을 가지고 할당 가능한 값들의 집합을 유추해야 한다는 뜻이다. 이 과정을 ts에서는 넓히기(widening)라고 부른다. ts는 "타입 넓히기(widening)" 과정을 거쳐서 타입을 추론한다

예를 들어, 아래와 같이 변수 x를 선언하고 null을 할당하는 경우,

let x = null; // x: any

ts는 null의 타입이 any의 하위 타입이므로, 변수 x의 타입을 any로 추론한다. 이는 변수 x가 any 타입을 갖게 되어, 다른 타입의 값도 할당할 수 있게 되는 문제가 있다. 이렇게 ts가 예측하지 못한 타입으로 변수의 타입을 확장하는 것을 타입 넓히기라고 한다.

타입 넓히기는 코드의 안정성을 해치기 때문에, 가능하면 변수에 초기값을 할당할 때 해당 변수의 타입을 명시적으로 지정하여 이를 방지할 수 있다. 예를 들어, 변수 x의 타입을 null로 지정해주면,

let x: null = null; // x: null

null만 할당 가능한 null 타입으로 x의 타입이 제한된다. 이렇게 명시적으로 타입을 지정하면, 변수의 타입이 예상과 다르게 추론되어 생기는 버그를 방지할 수 있다.

넓히기 과정을 제어하는 방법

  • const를 사용하기
  • 명시적인 타입 구문을 제공하기
  • 타입 체커에 추가적인 문맥을 제공하기(함수의 매개변수로 값을 전달)
  • const 단언문 사용 → as const 사용

-요약-

  • TypeScript가 넓히기를 통해 상수를 추론하는 방법에 대해 이해해야 한다.
  • 동작에 영향을 줄 수 있는 방법인 const, as const, 타입 구문, 문맥 등에 익숙해져야 한다.

0개의 댓글

관련 채용 정보