목록으로

프로그래밍 · TypeScript

TypeScript 완전정복 5화: 타입 추론과 타입 명시 이해하기

BeanCon
TypeScript 타입 추론과 타입 명시의 차이를 설명하는 대표 이미지

TypeScript 완전정복 5화입니다. 타입 추론과 타입 명시의 차이를 입문자 눈높이에서 정리했습니다. 기본 타입 추론, let과 const의 차이, 초기값이 없는 변수, 함수 매개변수와 반환 타입, 객체 타입, 문맥적 타이핑, any 사용의 위험, 실무에서 타입을 작성하는 기준과 판단 공식까지 다룹니다.

목차

타입 추론과 타입 명시의 차이

TypeScript를 처음 배우면 이런 생각이 듭니다.

“TypeScript니까 모든 변수에 타입을 적어야 하는 거 아닌가?”

그래서 코드가 점점 이렇게 변하기 시작합니다.

let name: string = "김타입";
let age: number = 25;
let isDeveloper: boolean = true;

물론 틀린 코드는 아닙니다.

하지만 TypeScript는 속으로 이렇게 말하고 있을지도 모릅니다.

“그 정도는 나도 알아… 굳이 다 말해주지 않아도 돼.”

TypeScript에는 개발자가 타입을 직접 적지 않아도 값과 코드의 흐름을 분석해 타입을 판단하는 능력이 있습니다.

바로 타입 추론(Type Inference)입니다.

이번 시간에는 다음 내용을 알아봅니다.

  • 타입 추론이란 무엇인가?
  • 타입 명시는 언제 필요한가?
  • `let`과 `const`의 추론 결과는 왜 다른가?
  • 함수의 매개변수와 반환값은 어떻게 처리해야 하는가?
  • 실무에서는 타입을 얼마나 적는 것이 좋은가?

이제 TypeScript가 어디까지 눈치챌 수 있는지 확인해 보겠습니다. 🕵️‍♂️

1. 타입 추론이란?

타입 추론은 TypeScript가 코드에 들어 있는 값을 분석하여 타입을 자동으로 결정하는 기능입니다.

다음 코드를 살펴보겠습니다.

let message = "Hello TypeScript";

우리는 `message` 옆에 `: string`을 작성하지 않았습니다.

하지만 TypeScript는 오른쪽 값을 확인합니다.

"Hello TypeScript"

이 값은 문자열입니다.

따라서 TypeScript는 내부적으로 다음과 같이 판단합니다.

let message: string = "Hello TypeScript";

직접 타입을 적지 않았지만 `message`는 이미 `string` 타입으로 결정된 것입니다.

그래서 다음 코드는 오류가 발생합니다.

let message = "Hello TypeScript";

message = 100;

TypeScript의 반응은 단호합니다.

Type 'number' is not assignable to type 'string'.

번역하면 다음과 같습니다.

“처음에 문자열이라고 해놓고 갑자기 숫자를 넣으면 곤란합니다.”

TypeScript는 한 번 추론한 타입을 기억하고 있습니다.

2. 기본 타입은 어떻게 추론될까?

TypeScript는 변수에 들어간 초기값을 보고 기본 타입을 추론합니다.

문자열 추론

let userName = "타입맨";

TypeScript가 추론한 타입은 다음과 같습니다.

string

따라서 문자열은 다시 저장할 수 있습니다.

userName = "자바스크립트맨";

하지만 숫자는 저장할 수 없습니다.

userName = 100;

숫자 추론

let level = 10;

`level`의 타입은 `number`로 추론됩니다.

level = 20;

숫자이므로 정상입니다.

level = "최고 레벨";

문자열이므로 오류가 발생합니다.

불리언 추론

let isLogin = true;

`isLogin`은 `boolean` 타입으로 추론됩니다.

isLogin = false;

정상입니다.

isLogin = "로그인 완료";

문자열을 넣었기 때문에 오류가 발생합니다.

배열 추론

배열도 내부에 들어 있는 값을 보고 타입을 판단합니다.

let scores = [80, 90, 100];

TypeScript는 `scores`를 다음 타입으로 추론합니다.

number[]

따라서 숫자는 추가할 수 있습니다.

scores.push(95);

하지만 문자열은 추가할 수 없습니다.

scores.push("만점");

배열 안에 여러 종류의 값이 있다면 어떻게 될까요?

let values = [1, "hello", true];

TypeScript는 다음과 같은 유니언 타입으로 추론합니다.

(number | string | boolean)[]

즉, 숫자와 문자열과 불리언 값을 저장할 수 있는 배열입니다.

TypeScript는 생각보다 눈치가 빠릅니다.

배열을 한 번 훑어보고 내부 구성원들의 출신 성분까지 파악합니다. 🔍

3. 타입 명시란?

타입 명시(Type Annotation)는 개발자가 변수나 함수에 타입을 직접 작성하는 것입니다.

다음 코드에서 `: string` 부분이 타입 명시입니다.

let userName: string = "타입맨";

숫자 타입도 직접 작성할 수 있습니다.

let age: number = 30;

불리언 타입도 마찬가지입니다.

let isAdmin: boolean = false;

타입 명시는 TypeScript에게 다음과 같이 말하는 것과 같습니다.

“이 변수에는 앞으로 반드시 이 타입만 들어와야 해.”

타입 추론이 TypeScript의 판단이라면, 타입 명시는 개발자의 선언입니다.

4. 타입 추론과 타입 명시는 무엇이 다를까?

다음 두 코드는 결과적으로 같은 타입을 가집니다.

let language = "TypeScript";
let language: string = "TypeScript";

첫 번째 코드는 TypeScript가 타입을 추론합니다.

두 번째 코드는 개발자가 타입을 명시합니다.

구분코드타입 결정 주체
타입 추론`let age = 20`TypeScript
타입 명시`let age: number = 20`개발자

두 방식 모두 `age`는 `number` 타입이 됩니다.

그렇다면 이런 의문이 생깁니다.

“결과가 같다면 타입을 직접 적을 필요가 없는 것 아닌가?”

초기값만 봐도 타입이 명확한 경우에는 굳이 작성하지 않아도 됩니다.

let count = 10;
let title = "TypeScript";
let isOpen = false;

이 코드에 타입을 모두 추가하면 다음과 같습니다.

let count: number = 10;
let title: string = "TypeScript";
let isOpen: boolean = false;

타입 정보가 반복되고 있습니다.

오른쪽의 값을 보면 이미 타입이 명확하기 때문입니다.

10            // number
"TypeScript"  // string
false         // boolean

이런 경우 TypeScript의 추론을 믿어도 좋습니다.

TypeScript에게 모든 것을 설명하려 들면 코드가 잔소리 많은 설명서처럼 변할 수 있습니다. 📚

5. `let`과 `const`는 추론 결과가 다르다

다음 두 변수를 비교해 보겠습니다.

let framework = "React";
const library = "React";

겉으로 보기에는 비슷하지만 TypeScript의 추론 결과는 다릅니다.

let framework = "React";

`framework`는 나중에 다른 문자열로 변경될 수 있습니다.

framework = "Vue";
framework = "Angular";

따라서 TypeScript는 `framework`를 넓은 범위인 `string` 타입으로 추론합니다.

let framework: string = "React";

반면 `const`는 값을 다시 할당할 수 없습니다.

const library = "React";

`library`에는 오직 `"React"`라는 값만 존재할 수 있습니다.

따라서 TypeScript는 다음처럼 더 구체적인 타입으로 추론합니다.

const library: "React" = "React";

여기서 `"React"`는 단순한 문자열 값이 아니라 문자열 리터럴 타입입니다.

`let`의 추론

let status = "ready";

추론 타입:

string

`const`의 추론

const status = "ready";

추론 타입:

"ready"

`let`은 변신 가능성이 있으므로 넓게 봅니다.

`const`는 값이 고정되어 있으므로 정밀하게 봅니다.

TypeScript는 `let`에게는 열린 가능성을 주고, `const`에게는 이름표를 꽉 붙여주는 셈입니다.

6. 초기값이 없으면 타입 명시가 필요할 수 있다

변수를 선언하면서 값을 바로 넣지 않는 경우가 있습니다.

let userName;

이 상태에서는 어떤 값이 들어올지 알 수 없습니다.

이후 다음과 같이 여러 타입을 넣을 수도 있습니다.

userName = "타입맨";
userName = 100;
userName = true;

설정에 따라 TypeScript가 값의 흐름을 분석할 수 있지만, 변수의 목적이 불분명해질 가능성이 큽니다.

변수에 저장할 타입을 알고 있다면 직접 명시하는 편이 좋습니다.

let userName: string;

이제 문자열만 저장할 수 있습니다.

userName = "타입맨";

숫자를 저장하면 오류가 발생합니다.

userName = 100;

초기값이 없는 변수는 아직 정체가 정해지지 않은 캐릭터와 같습니다.

타입을 지정해 주지 않으면 어느 장르로 튈지 모릅니다. 🎭

7. 함수의 매개변수는 타입을 직접 작성하자

변수의 타입은 초기값을 보고 추론할 수 있습니다.

하지만 함수의 매개변수는 상황이 다릅니다.

function greet(name) {
  return `안녕하세요, ${name}님`;
}

TypeScript는 `name`에 어떤 값이 들어올지 알 수 없습니다.

문자열일 수도 있습니다.

greet("타입맨");

숫자일 수도 있습니다.

greet(100);

객체가 들어올 수도 있습니다.

greet({ name: "타입맨" });

`noImplicitAny` 옵션이 활성화되어 있다면 다음과 같은 오류가 발생합니다.

Parameter 'name' implicitly has an 'any' type.

매개변수 `name`이 암묵적으로 `any` 타입을 가진다는 의미입니다.

함수의 매개변수에는 타입을 직접 작성하는 것이 기본입니다.

function greet(name: string) {
  return `안녕하세요, ${name}님`;
}

이제 문자열만 전달할 수 있습니다.

greet("타입맨");

숫자를 전달하면 오류가 발생합니다.

greet(100);

함수의 매개변수는 건물 입구의 출입 조건과 같습니다.

name: string

이 조건은 입구에 붙어 있는 안내문입니다.

“문자열만 입장할 수 있습니다.”

안내문이 없다면 숫자, 객체, 배열이 우르르 들어오면서 함수 내부가 북적일 수 있습니다.

8. 함수의 반환 타입은 추론할 수 있다

TypeScript는 함수 내부의 `return` 값을 분석해 반환 타입을 추론할 수 있습니다.

function add(a: number, b: number) {
  return a + b;
}

`a`와 `b`는 숫자입니다.

두 숫자를 더한 결과도 숫자입니다.

따라서 TypeScript는 반환 타입을 `number`로 추론합니다.

function add(a: number, b: number): number {
  return a + b;
}

반환 타입을 직접 작성하지 않아도 결과는 같습니다.

문자열 반환 타입 추론

function getMessage() {
  return "작업이 완료되었습니다.";
}

반환 타입은 `string`으로 추론됩니다.

불리언 반환 타입 추론

function isAdult(age: number) {
  return age >= 20;
}

반환 타입은 `boolean`으로 추론됩니다.

배열 반환 타입 추론

function getNumbers() {
  return [1, 2, 3];
}

반환 타입은 `number[]`로 추론됩니다.

함수의 반환 타입은 TypeScript가 비교적 잘 추론합니다.

하지만 모든 함수의 반환 타입을 생략하는 것이 정답은 아닙니다.

9. 반환 타입을 직접 작성하면 실수를 더 빨리 발견한다

다음 함수는 사용자의 나이를 반환하려고 만든 함수입니다.

function getUserAge() {
  return 30;
}

TypeScript는 반환 타입을 `number`로 추론합니다.

그런데 개발자가 실수로 문자열을 반환하도록 코드를 변경했다고 해보겠습니다.

function getUserAge() {
  return "30세";
}

TypeScript는 이 함수의 반환 타입을 `string`으로 다시 추론합니다.

문법적으로는 문제가 없습니다.

하지만 함수의 원래 목적이 숫자 나이를 반환하는 것이었다면 논리적인 오류입니다.

반환 타입을 명시하면 이런 실수를 바로 발견할 수 있습니다.

function getUserAge(): number {
  return "30세";
}

오류가 발생합니다.

Type 'string' is not assignable to type 'number'.

반환 타입 명시는 함수가 지켜야 할 계약서와 같습니다.

(): number

이 표시는 다음을 의미합니다.

“이 함수는 반드시 숫자를 반환하겠습니다.”

함수 내부에서 문자열을 반환하면 TypeScript가 계약 위반 경보를 울립니다. 🚨

10. 반환 타입 명시가 특히 유용한 경우

여러 사람이 사용하는 공용 함수

function calculateTotal(
  price: number,
  quantity: number
): number {
  return price * quantity;
}

반환 타입이 적혀 있으면 함수가 무엇을 반환하는지 바로 알 수 있습니다.

API 응답을 만드는 함수

type ApiResponse = {
  success: boolean;
  message: string;
};

function createResponse(): ApiResponse {
  return {
    success: true,
    message: "요청이 완료되었습니다.",
  };
}

반환 객체의 구조가 잘못되면 TypeScript가 오류를 알려줍니다.

function createResponse(): ApiResponse {
  return {
    success: true,
  };
}

`message`가 없으므로 오류가 발생합니다.

조건에 따라 여러 값을 반환하는 함수

function getDiscountRate(isMember: boolean): number {
  if (isMember) {
    return 0.1;
  }

  return 0;
}

반환 타입을 명시하면 모든 코드 경로가 올바른 값을 반환하는지 확인하기 쉬워집니다.

재귀 함수

function factorial(n: number): number {
  if (n <= 1) {
    return 1;
  }

  return n * factorial(n - 1);
}

재귀 함수는 함수 내부에서 자기 자신을 호출합니다.

이런 경우 반환 타입을 명시하면 코드의 의도가 더 분명해집니다.

11. 객체의 타입도 자동으로 추론된다

다음 객체를 살펴보겠습니다.

const user = {
  name: "타입맨",
  age: 30,
  isAdmin: false,
};

TypeScript는 객체의 각 속성을 분석합니다.

추론된 타입은 다음과 비슷합니다.

{
  name: string;
  age: number;
  isAdmin: boolean;
}

따라서 속성에 잘못된 타입의 값을 넣으면 오류가 발생합니다.

user.age = "서른 살";

`age`는 `number`로 추론되었기 때문입니다.

존재하지 않는 속성에 접근해도 오류가 발생합니다.

console.log(user.email);

`user` 객체에는 `email` 속성이 없습니다.

TypeScript는 객체를 대충 보는 척하면서 속성 하나하나를 출석부처럼 정리합니다.

12. 객체 타입은 언제 직접 작성해야 할까?

간단한 객체는 타입 추론만으로 충분할 수 있습니다.

const product = {
  name: "키보드",
  price: 50000,
};

하지만 같은 구조의 객체를 여러 곳에서 사용한다면 타입을 따로 정의하는 편이 좋습니다.

type Product = {
  name: string;
  price: number;
};

이제 여러 객체에 같은 타입을 적용할 수 있습니다.

const keyboard: Product = {
  name: "기계식 키보드",
  price: 120000,
};

const mouse: Product = {
  name: "무선 마우스",
  price: 60000,
};

함수의 매개변수와 반환 타입에도 사용할 수 있습니다.

function printProduct(product: Product): void {
  console.log(`${product.name}: ${product.price}원`);
}

타입을 별도로 정의하면 코드 전체에서 객체 구조를 일관되게 관리할 수 있습니다.

13. 문맥적 타이핑이란?

TypeScript는 오른쪽의 값만 보는 것이 아닙니다.

코드가 사용되는 문맥(Context)을 보고 타입을 추론하기도 합니다.

이를 문맥적 타이핑(Contextual Typing)이라고 합니다.

예를 들어 배열의 `forEach()`를 사용해 보겠습니다.

const numbers = [10, 20, 30];

numbers.forEach((number) => {
  console.log(number.toFixed(2));
});

콜백 함수의 매개변수 `number`에 타입을 작성하지 않았습니다.

하지만 TypeScript는 `numbers`가 `number[]`라는 사실을 알고 있습니다.

따라서 `forEach()`의 매개변수도 `number`라고 추론합니다.

마우스를 올려보면 다음과 같은 정보를 확인할 수 있습니다.

number: number

직접 작성할 필요가 없습니다.

numbers.forEach((number: number) => {
  console.log(number.toFixed(2));
});

이렇게 작성해도 되지만 이미 문맥을 통해 알 수 있는 타입을 반복한 셈입니다.

이벤트에서도 문맥적 타이핑이 사용된다

브라우저 환경에서 버튼 이벤트를 처리하는 코드를 살펴보겠습니다.

const button = document.querySelector("button");

button?.addEventListener("click", (event) => {
  console.log(event.type);
});

`event`에 타입을 적지 않았습니다.

하지만 TypeScript는 `"click"` 이벤트에 전달되는 객체의 타입을 문맥을 통해 추론합니다.

이처럼 TypeScript는 코드 주변을 살펴보고 빈칸을 채웁니다.

단순한 자동완성을 넘어, 코드의 분위기까지 읽는 셈입니다. 🧠

14. 타입 추론이 너무 넓게 되는 경우

TypeScript의 추론이 언제나 개발자의 의도와 정확히 일치하는 것은 아닙니다.

다음 객체를 살펴보겠습니다.

const request = {
  method: "GET",
  url: "/users",
};

개발자는 `method`가 `"GET"`이라는 고정된 값이라고 생각할 수 있습니다.

하지만 객체의 속성은 변경될 가능성이 있습니다.

request.method = "POST";

따라서 TypeScript는 `request.method`를 `"GET"`이 아니라 `string`으로 추론합니다.

이 상태에서 특정 리터럴 타입을 요구하는 함수에 전달하면 문제가 생길 수 있습니다.

type HttpMethod = "GET" | "POST";

function sendRequest(method: HttpMethod) {
  console.log(`${method} 요청 전송`);
}

sendRequest(request.method);

`request.method`가 일반 `string`으로 추론되었다면 오류가 발생할 수 있습니다.

이럴 때는 객체 타입을 명시할 수 있습니다.

type RequestConfig = {
  method: "GET" | "POST";
  url: string;
};

const request: RequestConfig = {
  method: "GET",
  url: "/users",
};

또는 `as const`를 사용할 수도 있습니다.

const request = {
  method: "GET",
  url: "/users",
} as const;

`as const`를 사용하면 객체의 속성이 읽기 전용이 되고 값 자체가 리터럴 타입으로 추론됩니다.

request.method;

추론 타입:

"GET"

`as const`는 TypeScript에게 이렇게 말하는 것과 같습니다.

“이 값들은 임시 메모가 아니라 돌에 새긴 글자입니다.”

15. `any`로 도망치면 타입 추론도 멈춘다

다음 코드를 살펴보겠습니다.

let data: any = "hello";

`data`가 `any` 타입이므로 어떤 값이든 저장할 수 있습니다.

data = 100;
data = true;
data = {};

문제는 TypeScript가 타입 검사를 거의 수행하지 않는다는 점입니다.

data.notExistingMethod();

실제로 존재하지 않는 메서드를 호출해도 컴파일 단계에서 오류를 발견하지 못할 수 있습니다.

`any`는 TypeScript의 감시 카메라를 잠시 꺼버리는 버튼과 비슷합니다.

편리하지만 너무 자주 누르면 TypeScript를 사용하는 의미가 흐려집니다.

타입을 알 수 없는 값이라면 `any`보다 `unknown`을 사용하는 편이 안전합니다.

let data: unknown = "hello";

`unknown` 타입의 값은 타입을 확인한 후에 사용할 수 있습니다.

if (typeof data === "string") {
  console.log(data.toUpperCase());
}

TypeScript는 조건문 내부에서 `data`가 문자열이라는 사실을 추론합니다.

이것을 타입 좁히기(Type Narrowing)라고 합니다.

타입 좁히기는 이후 회차에서 더욱 자세히 다룰 예정입니다.

16. 타입을 모두 작성하면 더 안전할까?

다음 코드를 살펴보겠습니다.

const name: string = "타입맨";
const age: number = 30;
const isAdmin: boolean = false;
const skills: string[] = ["JavaScript", "TypeScript"];

타입이 모두 적혀 있으므로 안전해 보입니다.

하지만 오른쪽의 초기값만 봐도 타입을 충분히 알 수 있습니다.

const name = "타입맨";
const age = 30;
const isAdmin = false;
const skills = ["JavaScript", "TypeScript"];

두 번째 코드도 타입 안전성을 유지합니다.

오히려 중복 정보가 줄어들어 읽기 편합니다.

타입을 많이 작성한다고 무조건 좋은 TypeScript 코드가 되는 것은 아닙니다.

좋은 TypeScript 코드는 필요한 곳에 정확한 타입을 작성하고, 명확한 곳에서는 추론을 활용합니다.

타입은 소금과 비슷합니다.

없으면 밋밋하지만, 모든 곳에 한 움큼씩 뿌리면 코드가 짜집니다. 🧂

17. 실무에서 권장하는 타입 작성 기준

타입 추론과 타입 명시 사이에서 고민된다면 다음 기준을 활용할 수 있습니다.

초기값으로 타입이 명확하면 추론 사용

const title = "TypeScript 완전정복";
const count = 5;
const isPublished = true;

타입을 직접 작성하지 않아도 의미가 분명합니다.

초기값이 없으면 타입 명시

let selectedUserId: number;
let errorMessage: string;

어떤 값이 들어갈지 미리 정해져 있다면 타입을 작성합니다.

함수의 매개변수는 타입 명시

function greet(name: string) {
  return `안녕하세요, ${name}님`;
}

함수에 어떤 값을 전달해야 하는지 분명하게 보여줍니다.

공개 함수의 반환 타입은 명시 고려

function calculatePrice(
  price: number,
  quantity: number
): number {
  return price * quantity;
}

다른 코드에서 많이 사용하는 함수라면 반환 타입을 작성하는 것이 좋습니다.

복잡한 객체는 타입이나 인터페이스 정의

type User = {
  id: number;
  name: string;
  email: string;
};

객체 구조를 재사용하고 관리하기 쉬워집니다.

콜백 매개변수는 문맥적 추론 활용

const numbers = [1, 2, 3];

numbers.map((number) => number * 2);

`number`의 타입은 이미 `number`로 추론되므로 반복해서 작성하지 않아도 됩니다.

18. 타입 추론과 타입 명시 비교 예제

타입을 지나치게 많이 작성한 코드

const userName: string = "타입맨";
const userAge: number = 30;
const isAdmin: boolean = false;

const numbers: number[] = [1, 2, 3];

const doubledNumbers: number[] = numbers.map(
  (number: number): number => {
    return number * 2;
  }
);

타입 정보가 정확하지만 반복이 많습니다.

타입 추론을 적절히 활용한 코드

const userName = "타입맨";
const userAge = 30;
const isAdmin = false;

const numbers = [1, 2, 3];

const doubledNumbers = numbers.map((number) => {
  return number * 2;
});

TypeScript는 다음 내용을 모두 추론할 수 있습니다.

  • `userName`은 문자열
  • `userAge`는 숫자
  • `isAdmin`은 불리언
  • `numbers`는 숫자 배열
  • `number`는 숫자
  • `doubledNumbers`는 숫자 배열

코드는 짧아졌지만 타입 안전성은 그대로 유지됩니다.

19. 반환 타입 명시로 버그 찾기

사용자 이름을 반환하는 함수를 작성해 보겠습니다.

function getUserName(userId: number) {
  if (userId === 1) {
    return "타입맨";
  }
}

`userId`가 `1`이 아닐 경우 아무 값도 반환하지 않습니다.

TypeScript는 반환 타입을 다음처럼 추론할 수 있습니다.

string | undefined

하지만 개발자는 항상 문자열을 반환하려고 했을 수 있습니다.

반환 타입을 명시하면 문제를 발견하기 쉬워집니다.

function getUserName(userId: number): string {
  if (userId === 1) {
    return "타입맨";
  }
}

모든 코드 경로에서 문자열이 반환되지 않으므로 오류가 발생합니다.

수정 방법은 기본값을 반환하는 것입니다.

function getUserName(userId: number): string {
  if (userId === 1) {
    return "타입맨";
  }

  return "알 수 없는 사용자";
}

반환 타입 명시는 함수 내부에 빠진 탈출구가 없는지 확인하는 보안요원 역할도 합니다.

20. 실습: 상품 가격 계산기 만들기

상품 정보를 나타내는 타입을 만들어 보겠습니다.

type Product = {
  name: string;
  price: number;
  quantity: number;
};

상품 객체를 생성합니다.

const product: Product = {
  name: "기계식 키보드",
  price: 120000,
  quantity: 2,
};

총가격을 계산하는 함수를 작성합니다.

function calculateTotal(product: Product): number {
  return product.price * product.quantity;
}

결과를 출력합니다.

const totalPrice = calculateTotal(product);

console.log(`${product.name} 총가격: ${totalPrice}원`);

전체 코드는 다음과 같습니다.

type Product = {
  name: string;
  price: number;
  quantity: number;
};

const product: Product = {
  name: "기계식 키보드",
  price: 120000,
  quantity: 2,
};

function calculateTotal(product: Product): number {
  return product.price * product.quantity;
}

const totalPrice = calculateTotal(product);

console.log(`${product.name} 총가격: ${totalPrice}원`);

여기에서 타입 추론과 타입 명시가 함께 사용되었습니다.

타입을 명시한 부분

const product: Product
product: Product
: number

상품의 구조와 함수의 계약을 명확하게 나타냅니다.

타입을 추론한 부분

const totalPrice = calculateTotal(product);

`calculateTotal()` 함수가 `number`를 반환하므로 `totalPrice`도 자동으로 `number`가 됩니다.

모든 곳에 타입을 작성하지 않고 필요한 경계에만 작성했습니다.

이것이 실무에서 자주 사용하는 균형 잡힌 방식입니다.

21. 실습: 회원 등급 판별하기

회원 점수에 따라 등급을 반환하는 함수를 작성해 보겠습니다.

type Grade = "BRONZE" | "SILVER" | "GOLD";

반환 가능한 문자열을 세 가지로 제한했습니다.

function getGrade(score: number): Grade {
  if (score >= 90) {
    return "GOLD";
  }

  if (score >= 70) {
    return "SILVER";
  }

  return "BRONZE";
}

함수를 호출합니다.

const userGrade = getGrade(85);

console.log(userGrade);

출력 결과는 다음과 같습니다.

SILVER

잘못된 값을 반환하면 TypeScript가 오류를 알려줍니다.

function getGrade(score: number): Grade {
  return "DIAMOND";
}

`Grade` 타입에는 `"DIAMOND"`가 없으므로 오류가 발생합니다.

타입 명시를 통해 함수가 반환할 수 있는 값을 정확히 제한한 것입니다.

22. 초보자가 자주 하는 실수

실수 1. 모든 변수에 타입 작성하기

const message: string = "hello";
const count: number = 10;
const isOpen: boolean = true;

틀린 코드는 아니지만 불필요한 중복이 많습니다.

const message = "hello";
const count = 10;
const isOpen = true;

초기값이 명확하면 추론을 활용할 수 있습니다.

실수 2. 함수 매개변수 타입 생략하기

function multiply(a, b) {
  return a * b;
}

매개변수 타입은 직접 작성하는 편이 좋습니다.

function multiply(a: number, b: number) {
  return a * b;
}

실수 3. 모르겠으면 무조건 `any` 사용하기

function printValue(value: any) {
  console.log(value.toUpperCase());
}

숫자가 전달되면 실행 중 오류가 발생할 수 있습니다.

printValue(100);

타입을 알 수 없다면 `unknown`으로 받고 확인 후 사용합니다.

function printValue(value: unknown) {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  }
}

실수 4. 반환 타입을 신경 쓰지 않기

function getPrice() {
  return "10000";
}

가격을 숫자로 사용하려 했다면 문제가 될 수 있습니다.

function getPrice(): number {
  return 10000;
}

함수의 역할이 중요할수록 반환 타입을 명확하게 작성하는 것이 좋습니다.

23. 타입 작성 판단 공식

타입을 작성해야 할지 고민된다면 다음 질문을 순서대로 확인해 보세요.

질문 1. 초기값만 봐도 타입이 명확한가?

const count = 10;

그렇다면 타입 추론을 사용합니다.

질문 2. 변수에 초기값이 없는가?

let count;

그렇다면 타입 명시를 고려합니다.

let count: number;

질문 3. 함수 외부에서 값이 들어오는가?

function updateUser(name) {
  // ...
}

매개변수 타입을 명시합니다.

function updateUser(name: string) {
  // ...
}

질문 4. 함수가 반드시 특정 값을 반환해야 하는가?

function getUser(): User {
  // ...
}

반환 타입을 명시하여 함수의 계약을 고정합니다.

질문 5. 여러 곳에서 같은 객체 구조를 사용하는가?

그렇다면 `type`이나 `interface`로 정의합니다.

type User = {
  id: number;
  name: string;
};

24. 미니 퀴즈

문제 1

다음 코드에서 `message`의 타입은 무엇일까요?

let message = "Hello";

정답

string

`let` 변수는 다른 문자열로 변경될 수 있으므로 `string`으로 추론됩니다.

문제 2

다음 코드에서 `status`의 타입은 무엇일까요?

const status = "success";

정답

"success"

`const`는 재할당할 수 없으므로 문자열 리터럴 타입으로 추론됩니다.

문제 3

다음 함수에서 생략된 타입을 작성해 보세요.

function add(a, b) {
  return a + b;
}

정답

function add(a: number, b: number): number {
  return a + b;
}

반환 타입은 생략해도 `number`로 추론되지만, 함수의 역할을 명확하게 보여주기 위해 작성할 수 있습니다.

문제 4

다음 코드에는 어떤 문제가 있을까요?

let data: any = "hello";

data.notExistingMethod();

정답

`any`를 사용하면 TypeScript의 타입 검사가 약해집니다.

존재하지 않는 메서드를 호출해도 컴파일 단계에서 발견하지 못할 수 있습니다.

문제 5

다음 코드에서 `numbers`의 타입은 무엇일까요?

const numbers = [1, 2, 3];

정답

number[]

배열 내부의 값이 모두 숫자이므로 숫자 배열로 추론됩니다.

25. 핵심 정리

타입 추론

TypeScript가 값과 코드의 문맥을 분석해 타입을 자동으로 결정하는 기능입니다.

const name = "타입맨";

`name`의 타입은 자동으로 추론됩니다.

타입 명시

개발자가 타입을 직접 작성하는 방식입니다.

let name: string;

변수나 함수가 지켜야 할 타입을 명확하게 선언할 수 있습니다.

추론을 사용하기 좋은 경우

const count = 10;
const message = "hello";
const isOpen = true;

초기값만으로 타입이 명확한 경우입니다.

타입을 명시하기 좋은 경우

let selectedId: number;

function greet(name: string): string {
  return `안녕하세요, ${name}님`;
}

초기값이 없거나 함수의 입력과 출력을 명확하게 제한해야 하는 경우입니다.

26. 마무리

TypeScript를 배운다고 해서 모든 변수 옆에 타입을 붙여야 하는 것은 아닙니다.

TypeScript는 생각보다 많은 타입을 스스로 추론할 수 있습니다.

const language = "TypeScript";
const version = 5;
const isUseful = true;

이 코드의 타입을 TypeScript는 이미 알고 있습니다.

반면 함수의 매개변수, 복잡한 객체, 외부에 공개되는 함수의 반환값처럼 프로그램의 경계가 되는 부분에서는 타입을 명시하는 것이 좋습니다.

핵심은 다음 한 문장으로 정리할 수 있습니다.

명확한 곳에서는 TypeScript의 추론을 믿고, 중요한 경계에서는 타입을 직접 선언하자.

타입을 전혀 작성하지 않는 것도 문제지만, 모든 곳에 타입을 반복해서 작성하는 것도 좋은 방법은 아닙니다.

좋은 TypeScript 코드는 개발자와 컴파일러가 역할을 나눕니다.

개발자는 의도를 알려주고, TypeScript는 나머지 빈칸을 채웁니다.

둘의 호흡이 맞는 순간 코드는 짧아지고, 오류는 줄어들며, 개발자는 조금 더 평화로워집니다. ☕

27. 다음 편 예고

지금까지 우리는 TypeScript가 값과 코드의 문맥을 살펴보고 타입을 자동으로 결정하는 타입 추론을 알아봤습니다.

const userName = "김타입";
const age = 25;
const isLearning = true;

TypeScript는 각각의 값을 보고 `string`, `number`, `boolean` 타입을 알아낼 수 있었습니다.

하지만 TypeScript에서 사용할 수 있는 타입은 이것이 전부가 아닙니다.

const skills: string[] = ["JavaScript", "TypeScript"];
const location: [number, number] = [37.5, 127.0];

let emptyValue: null = null;
let unknownValue: unknown;

배열에는 어떤 방식으로 타입을 작성해야 할까요?

여러 값을 정해진 순서로 저장하는 튜플은 일반 배열과 무엇이 다를까요?

`null`과 `undefined`는 모두 값이 없다는 뜻인데, 왜 별도의 타입으로 구분할까요?

그리고 이름부터 자유분방한 `any`, 정체를 숨기고 있는 `unknown`, 아무것도 반환하지 않는 `void`는 언제 사용해야 할까요?

다음 편에서는 TypeScript 세계를 구성하는 여러 가지 기본 타입을 하나씩 살펴보겠습니다.

다음 이야기

[TypeScript 완전정복 #6] TypeScript 기본 타입 완벽 이해하기 | string부터 unknown까지 한 번에 정리

  • `string`, `number`, `boolean`은 어떻게 사용할까요?
  • 배열 타입을 작성하는 방법은 무엇일까요?
  • 튜플은 일반 배열과 무엇이 다를까요?
  • `null`과 `undefined`는 어떤 차이가 있을까요?
  • `any`를 함부로 사용하면 왜 위험할까요?
  • `unknown`은 `any`보다 왜 안전할까요?
  • `void`와 `never`는 언제 사용할까요?

다음 편에서는 문자열, 숫자, 불리언부터 배열, 튜플, `unknown`, `never`까지 TypeScript의 주요 타입을 한자리에서 만나보겠습니다.

댓글

0

댓글을 불러오는 중입니다.