[TypeScript] 타입 추론, 단언, 좁히기

ttining·2025년 3월 26일

타입 추론 (Type Inference)

1. 타입스크립트가 타입을 추론하는 원리

타입스크립트는 변수의 초기값을 기반으로 타입을 자동으로 추론한다.

  • 초기값이 있는 경우 : 해당 값의 타입을 기반으로 추론 ✅
  • 초기값이 없는 경우 : any 타입으로 추론 (암묵적 any, 권장 ❌)
let a = 10; // number 타입으로 추론
let b = "hello"; // string 타입으로 추론
let c = {
  id: 1,
  name: "ttining",
  profile: {
    nickname: "ttining._.",
  },
  urls: ["https://github.com/ttining-real"],
};

// 구조 분해 할당 시에도 타입 추론이 적용됨
let { id, name, profile } = c;

let [one, two, three] = [1, "hello", true];
// one: number, two: string, three: boolean으로 추론됨

2. let 🆚 const (타입 넓히기 포함)

1️⃣ let 키워드와 타입 넓히기

let으로 선언한 변수는 타입스크립트가 보다 범용적인 타입으로 넓혀서 추론한다.

// 초기값이 없을 경우 → 암묵적 any (타입 진화 가능)
let d; // d: any
d = 10; // d: number 타입으로 진화
d.toFixed();

d = "hello"; // d: string 타입으로 진화
d.toUpperCase();
// 초기값이 있는 경우 → 타입 넓히기 적용
let num = 10; // number 타입으로 추론됨 (10이 아닌 number)
let str = "hello"; // string 타입으로 추론됨

📌 타입 넓히기 (Widening)

  • 타입스크립트는 변수를 더 범용적으로 사용할 수 있도록 타입을 넓혀서 추론한다.
  • let으로 선언한 경우 10 → number, "hello" → string처럼 넓은 타입으로 변환된다.
  • 하지만 const로 선언하면 리터럴 타입이 유지된다.

2️⃣ const 키워드와 리터럴 타입

const로 선언하면 값이 변경될 수 없으므로, 해당 값의 리터럴 타입을 유지한다.

const num = 10; // 10 (리터럴 타입)
const str = "hello"; // "hello" (리터럴 타입)

즉, const를 사용하면 해당 값 그대로의 타입을 갖는다.
이와 다르게 let을 사용하면 더 넓은 타입으로 추론된다.

let num1 = 10; // number
const num2 = 10; // 10 (리터럴)

3. any 타입의 진화 (Evolution of any)

1️⃣ 암묵적 any

타입스크립트에서는 초기값이 없는 변수는 any로 추론되며, 이후 할당된 값에 따라 타입이 진화한다.

let x; // any
x = 10; // number
x.toFixed();

x = "hello"; // string
x.toUpperCase();

2️⃣ 명시적 any

그러나 명시적으로 any를 선언한 경우에는 진화가 발생하지 않는다.

let y: any;
y = 10; // any 유지
y = "hello"; // any 유지

📌 암묵적 any와 명시적 any

  • 암묵적인 any는 타입 진화를 거치지만, 명시적인 any는 항상 any로 유지된다.
  • 암묵적 any는 권장되지 않는다. → noImplicitAny 설정 활성화 권장

4. 배열과 객체에서의 타입 추론

배열과 객체도 타입스크립트가 자동으로 타입을 추론한다.

1️⃣ 배열 타입 추론

초기값을 배열처럼 다양한 타입의 값을 담을 수 있는 값으로 할당하면,
타입스크립트가 모든 배열 요소들의 타입을 비교해서 최적의 공통 타입으로 타입을 추론한다.

let arr = [1, "hello", true]; // (string | number | boolean)[] Union 타입으로 추론됨

2️⃣ 객체 타입 추론

객체의 경우도 내부 속성들의 타입을 기반으로 추론한다.

let obj = {
  name: "Alice",
  age: 25,
};

let { name, age } = obj;
// name: string, age: number로 추론됨

5. 함수 반환값의 타입 추론

타입스크립트는 함수의 반환값도 자동으로 추론한다.

function greet() {
  return "Hello"; // string으로 추론됨
}

function add(x: number, y: number) {
  return x + y; // number로 추론됨
}

void 반환 타입도 추론된다.

function logMessage(): void {
  console.log("Hello");
}

하지만, 명시적으로 반환 타입을 지정하는 것이 더 명확하고 안전하다. (특히, API 응답 타입이 변할 가능성이 있는 경우)

function fetchUser(): { id: number; name: string } {
  return { id: 1, name: "Alice" };
}



타입 단언 (Type Assertion)

타입 단언은 개발자가 타입스크립트의 타입 추론을 무시하고 특정 타입으로 강제하는 방법이다.
그러나 이는 타입스크립트의 타입 검사 기능을 우회하는 것으로, 값이 기대한 타입과 다를 경우 런타임 오류가 발생할 수 있다.

1. 기본 문법과 규칙

값 as 단언  # 단언식
  • A as B 형태로 사용하며, AB의 슈퍼타입이거나 서브타입이어야 한다.
  • 강제로 타입을 변경하는 것이므로, 타입 안정성을 해칠 수 있다.
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

2. 예시

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

// 빈 객체를 Person 타입으로 단언
let person = {} as Person;
person.name = "ttining";
person.age = 100;

// 초과 프로퍼티가 있는 객체를 다른 타입으로 단언
type Dog = {
  name: string;
  color: string;
};

let dog = {
  name: "꽝꽝이",
  color: "brown",
  breed: "진도",
} as Dog;

⚠️ 주의

타입 단언을 남용하면 TypeScript의 타입 안전성을 해칠 수 있다.
가능한 한 타입 가드나 타입 좁히기 방법을 사용하고, 타입 단언은 정말 필요할 때만 사용하는 것이 좋다.


3. const 단언 (as const)

  • const 단언을 사용하면 객체의 프로퍼티를 읽기 전용(readonly)으로 만들고, 값의 타입을 리터럴 타입으로 고정할 수 있다.
  • 객체 타입과 함께 사용할 때 활용도가 높다.
let num4 = 10 as const; // number 리터럴 타입 (10)

let cat = {
  name: "야옹이",
  color: "yellow",
} as const;

// cat.name을 변경하려고 하면 오류 발생
cat.name = "두식이"; // ❌ 오류 발생: 읽기 전용 속성

4. Non-null 단언 (!)

값이 null 또는 undefined가 아님을 보장할 때 Non-null 단언 연산자 !를 사용한다.
단, 실제로 null이나 undefined인 경우 런타임 오류가 발생할 수 있다.

type Post = {
  title: string;
  author?: string;
};

let post: Post = {
  title: "게시글1",
  author: "ttining",
};

const length: number = post.author?.length; // number | undefined

// author가 존재한다고 확신하는 경우
const len: number = post.author!.length; // author! -> null이 아님을 단언

⚠️ 주의

! 연산자를 사용하면 TypeScript의 타입 검사를 우회하므로, 값이 실제로 null일 경우 런타임 오류가 발생할 수 있다.
가급적이면 if 문을 사용해 null 체크를 수행하는 것이 더 안전하다.

if (post.author) {
  const len = post.author.length;
}



타입 좁히기 (Type Narrowing)

타입 좁히기는 조건문 등을 사용하여 넓은 타입에서 좁은 타입으로 제한하는 기법이다.
이를 통해 TypeScript가 해당 값에 대해 더 정확한 타입을 추론할 수 있도록 도와준다.

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

// value => number : toFixed
// value => string : toUpperCase
// value => Date : getTime
// value => PErson : name은 age살 입니다.
function func(value: number | string | Date | null | Person) {
  if (typeof value === "number") {
    console.log(value.toFixed());
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else if (value instanceof Date) {
    console.log(value.getTime());
  } else if (value && "age" in value) {
    console.log(`${value.name}${value.age}살 입니다.`);
  }
}

1. typeof, instanceof, in을 이용한 좁히기

1️⃣ typeof 연산자를 사용한 타입 좁히기

typeof를 사용하면 기본 타입(string, number, boolean, symbol, bigint, undefined, object, function)을 구분할 수 있다.

function printLength(value: string | number) {
  if (typeof value === "string") {
    console.log(value.length);
  } else {
    console.log(value.toFixed());
  }
}

2️⃣ instanceof 연산자를 사용한 타입 좁히기

instanceof를 사용하면 객체가 특정 클래스의 인스턴스인지 확인할 수 있다.

function printDate(value: Date | string) {
  if (value instanceof Date) {
    console.log(value.getTime());
  } else {
    console.log(value.toUpperCase());
  }
}

3️⃣ in 연산자를 사용한 타입 좁히기

in 연산자는 객체가 특정 속성을 가지고 있는지 확인하는 방법으로 타입을 좁힐 수 있다.

type Cat = { meow: () => void };
type Dog = { bark: () => void };

function makeSound(animal: Cat | Dog) {
  if ("meow" in animal) {
    animal.meow();
  } else {
    animal.bark();
  }
}

2. 명시적인 타입 검사를 통한 좁히기

typeof, instanceof, in 연산자로 좁히기 어려운 경우, 직접 프로퍼티를 검사하는 방식을 사용할 수 있다.

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ((animal as Fish).swim) {
    (animal as Fish).swim();
  } else {
    (animal as Bird).fly();
  }
}

⚠️ 주의

위 방법은 타입 단언(as

)을 사용하므로, 타입 안전성이 떨어질 위험이 있다.

대신 사용자 정의 타입 가드를 사용하는 것이 더 안전한 방법이다.


3. 사용자 정의 타입 가드 (is 연산자)

is 연산자를 사용하여 안전한 타입 좁히기 함수를 직접 정의할 수 있다.

type Car = { drive: () => void };
type Bike = { ride: () => void };

function isCar(vehicle: Car | Bike): vehicle is Car {
  return (vehicle as Car).drive !== undefined;
}

function useVehicle(vehicle: Car | Bike) {
  if (isCar(vehicle)) {
    vehicle.drive();
  } else {
    vehicle.ride();
  }
}

✅ 장점

isCar 함수는 vehicleCar 타입인지 안전하게 확인해주므로, as를 남발하지 않아도 된다.
이를 통해 더 안전한 타입 좁히기가 가능해진다.


🎯 정리

구분설명
typeof기본 타입 (string, number 등) 구분
instanceof특정 클래스의 인스턴스인지 확인
in객체가 특정 프로퍼티를 가지고 있는지 확인
명시적 타입 검사강제적인 타입 단언(as)을 사용한 방법 (권장 ❌)
사용자 정의 타입 가드(is)안전한 타입 좁히기 방법 (권장 ⭕)
profile
내가 보려고 만든 벨로그 *'-'*

0개의 댓글