어제 오늘 내일

[TypeScript] 4. 타입 추론 — 언제 타입을 직접 쓰고 언제 맡길 것인가 본문

IT/TypeScript

[TypeScript] 4. 타입 추론 — 언제 타입을 직접 쓰고 언제 맡길 것인가

hi.anna 2026. 8. 9. 12:42

이 글의 목표: 모든 곳에 타입을 다는 건 초보의 실수입니다. TypeScript가 알아서 추론하는 곳과, 직접 명시해야 하는 곳을 구분합니다.

TypeScript는 대부분 스스로 안다

타입을 명시하지 않아도, 값을 보고 타입을 추론합니다.

let count = 10;        // TypeScript는 count를 number로 추론
let title = "안녕";     // string으로 추론
let flag = true;       // boolean으로 추론

// count = "열";        // ❌ 이미 number로 추론됐으므로 에러

: number 같은 표기를 굳이 붙이지 않아도 됩니다. 오히려 붙이면 코드만 장황해집니다.

// 👎 불필요한 타입 표기
let count: number = 10;

// 👍 추론에 맡긴다
let count = 10;

let vs const: 추론 결과가 다르다

let a = "hello"; // 타입: string  (재할당 가능하므로 넓게 추론)
const b = "hello"; // 타입: "hello"  (재할당 불가하므로 리터럴로 좁게 추론)

const는 값이 바뀌지 않으므로, TypeScript가 리터럴 타입으로 좁혀 추론합니다. 이 차이는 나중에 유니온을 다룰 때 중요해집니다.

함수 반환 타입도 추론된다

// 반환 타입을 안 써도 number로 추론된다
function add(a: number, b: number) {
  return a + b;
}
const result = add(1, 2); // result는 number

다만 매개변수 타입은 추론되지 않습니다. 함수를 호출하는 쪽을 미리 알 수 없기 때문입니다.

// 👎 매개변수에 타입이 없으면 암묵적 any → strict 모드에서 에러
// function greet(name) { return `Hi ${name}`; }

// 👍 매개변수는 직접 명시해야 한다
function greet(name: string) {
  return `Hi ${name}`;
}

그럼 반환 타입은 써야 할까, 말아야 할까?

이건 취향과 상황이 갈립니다. 저는 공개 API(라이브러리, 모듈 경계)에는 명시하고, 내부 헬퍼 함수는 추론에 맡기는 편을 권합니다.

// 👍 명시하면, 실수로 잘못된 값을 반환할 때 함수 안에서 바로 잡힌다
function calculateTax(price: number): number {
  // 만약 실수로 문자열을 반환하면 이 함수 내부에서 에러 발생
  return price * 0.1;
}

반환 타입을 명시하면, 버그가 "함수를 호출하는 곳"이 아니라 "함수 정의"에서 잡힙니다.

언제 타입을 직접 써야 하나

1) 매개변수 (항상)

function format(value: number, digits: number) { /* ... */ }

2) 값 없이 선언만 할 때

let selectedId: number | null = null; // 지금은 null이지만 나중에 number가 들어온다
// 이때 타입을 안 쓰면 null 타입으로 굳어져 number를 못 넣는다

3) 추론이 너무 넓거나 좁을 때

// 추론에 맡기면 string[]으로 넓어진다
const roles = ["admin", "user"]; // string[]

// as const 를 붙이면 값이 바뀌지 않는 "고정된 배열"로 취급되어,
// 원소도 넓은 string이 아니라 리터럴 그대로 유지된다
const roles2 = ["admin", "user"] as const; // readonly ["admin", "user"]

// 위 배열로부터 "admin" | "user" 유니온 타입을 뽑아낸다
type Role = typeof roles2[number]; // "admin" | "user"

마지막 줄 typeof roles2[number]가 처음엔 낯설 텐데, 두 조각으로 나눠 보면 쉽습니다.

  • typeof roles2: 변수 roles2타입을 가져옵니다. 즉 readonly ["admin", "user"]. (typeof를 타입 자리에서 쓰면 "이 값의 타입"이라는 뜻입니다.)
  • [number]: 배열 타입에 number로 인덱스 접근을 하면 "모든 위치의 원소 타입"을 유니온으로 꺼냅니다. ["admin", "user"]의 0번, 1번 원소 타입을 합쳐 "admin" | "user"가 됩니다.

정리하면 "roles2 배열의 원소들이 가질 수 있는 값의 유니온"을 자동으로 만든 것입니다. 이렇게 하면 배열(값)과 타입이 항상 한 곳에서 관리되어, roles2에 새 역할을 추가하면 Role 타입도 자동으로 따라옵니다.

// 예: 배열에 "guest"를 추가하면
const roles2 = ["admin", "user", "guest"] as const;
type Role = typeof roles2[number]; // "admin" | "user" | "guest" 로 자동 확장

4) 빈 배열/객체로 시작할 때

// 👎 빈 배열은 any[]로 추론되어 안전성을 잃는다
const items = []; // any[]
items.push(1);
items.push("문자열"); // 막지 못함

// 👍 무엇을 담을지 명시한다
const nums: number[] = [];
nums.push(1);
// nums.push("x"); // ❌ 이제 막힌다

원칙: "값이 있으면 맡기고, 값이 없거나 넓으면 명시하라"

// 값이 명확 → 추론에 맡긴다
const total = 1000;
const user = { id: 1, name: "철수" };

// 값이 아직 없거나(변수만 선언), 추론이 원하는 것보다 넓을 때 → 명시
let response: User | null = null;
const config: Record<string, boolean> = {};

정리

  • TypeScript는 대부분의 지역 변수와 반환 타입을 알아서 추론한다. 불필요한 타입 표기는 오히려 코드를 지저분하게 만든다.
  • 매개변수는 항상 명시한다.
  • 값 없이 선언만 하거나, 빈 배열/객체로 시작하거나, 추론이 너무 넓을 때는 명시한다.
  • 공개 함수의 반환 타입을 명시하면 버그를 정의 자리에서 잡을 수 있다.

여기까지가 Part 1입니다. 다음 글부터 타입 시스템의 핵심으로 들어갑니다.

반응형
Comments