Frontend/TypeScript

[TypeScript] 2-2. 타입스크립트 이해하기 - 추론과 단언

ayeongjin 2025. 1. 24. 00:01

타입 추론

타입스크립트는 타입이 정의되어 있지 않은 변수의 타입을 자동으로 추론한다.

ex. 타입 추론 가능한 경우

let a = 10;
// number 타입으로 추론

ex. 타입 추론 불가능한 경우

function func(param){ // 오류
}
  • 타입 추론이 불가능한 변수(ex 매개변수)에는 암시적으로 any 타입이 추론된다.
  • 그러나 엄격한 타입 검사 모드 (tsconfig.json의 strict 옵션을 true로 설정)에서는 이런 암시적 any 타입의 추론을 오류로 판단

1. 타입 추론이 가능한 상황들

1) 변수 선언

let a = 10;
// number 타입으로 추론

let b = "hello";
// string 타입으로 추론

let c = {
  id: 1,
  name: "이정환",
  profile: {
    nickname: "winterlood",
  },
  urls: ["<https://winterlood.com>"],
};
// id, name, profile, urls 프로퍼티가 있는 객체 타입으로 추론

2) 구조 분해 할당

let { id, name, profile } = c;

let [one, two, three] = [1, "hello", true];

3) 함수의 반환값

함수 반환값의 타입은 return 문을 기준으로 잘 추론

function func() {
  return "hello";
}
// 반환값이 string 타입으로 추론된다

4) 기본값이 설정된 매개변수

  • 기본값이 설정된 매개변수의 타입은 기본값을 기준으로 추론
function func(message = "hello") {
  return "hello";
}

2. 주의해야 할 상황들

1) 암시적으로 any 타입으로 추론

  • 변수를 선언할때 초기값을 생략하면 암시적인 any 타입으로 추론
  • 참고로 이때 매개변수의 타입이 암시적 any로 추론될 때와 달리 일반 변수의 타입이 암시적 any 타입으로 추론되는 상황은 오류로 판단하지 않음
let d;
// 암시적인 any 타입으로 추론
  • 그리고 이 변수에 값을 할당하면 그 다음 라인부터 any 타입이 해당 값의 타입으로 변화합니다.
let d;
d = 10;
d.toFixed();

d = "hello";
d.toUpperCase();
d.toFixed(); // 오류
  • d = 10; 다음 라인부터는 d가 number 타입이 되고, d = “hello” 다음 라인부터는 d가 string 타입이 된다.
  • 따라서 마지막 라인에서 d가 string 타입일 때 toFixed 같은 number 타입의 메서드를 사용하려고 하면 오류가 발생
  • 이렇게 암시적으로 추론된 any 타입은 코드의 흐름에 따라 타입이 계속 변화 (이를 any의 진화라고 표현하기도 한다)

2) const 상수의 추론

상수는 초기화 때 설정한 값을 변경할 수 없기 때문에 특별히 가장 좁은 타입으로 추론된다

const num = 10;
// 10 Number Literal 타입으로 추론

const str = "hello";
// "hello" String Literal 타입으로 추론

3) 최적 공통 타입(Best Common Type)

  • 다음과 같이 다양한 타입의 요소를 담은 배열을 변수의 초기값으로 설정하면, 최적의 공통 타입으로 추론된다.
let arr = [1, "string"];
// (string | number)[] 타입으로 추론

타입 단언

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

let person: Person = {}; // 오류
person.name = "";
person.age = 23;
  • 변수 person은 Person 타입으로 정의 되었지만 초기화 할 때에는 빈 객체를 넣어두고 싶다고 가정
  • 그러나 타입스크립트에서는 이런 경우를 허용하지 않는다.
  • (빈 객체는 Person 타입이 아니므로 오류가 발생)
  • 이런 상황에서 빈 객체를 Person 타입이라고 타입스크립트에게 단언 ( 값 as 타입 )
type Person = {
  name: string;
  age: number;
};

let person = {} as Person;
person.name = "";
person.age = 23;
  • 타입 단언으로 초과 프로퍼티 검사 피하기
type Dog = {
  name: string;
  color: string;
};

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

1. 타입 단언의 조건

값 as 타입 형식의 단언식을 A as B로 표현했을 때 아래의 두가지 조건중 한가지를 반드시 만족해야 한다

  • A가 B의 슈퍼타입이다
  • A가 B의 서브타입이다
let num1 = 10 as never;   // ✅
let num2 = 10 as unknown; // ✅

let num3 = 10 as string;  // ❌
  1. num1은 A(number 타입)의 값을 B(never) 타입으로 단언 → never 타입은 모든 타입의 서브타입이므로 A가 B의 슈퍼타입
  2. num2는 A(number 타입)의 값을 B(unknown) 타입으로 단언 → unknown 타입은 모든 타입의 슈퍼타입이므로 A가 B의 서브타입
  3. num3는 A(number 타입)의 값을 B(string) 타입으로 단언 → 그러나 number 타입과 string 타입은 서로 슈퍼-서브 타입 관계를 갖지 않음

2. 다중 단언

let num3 = 10 as unknown as string;
  • 정말 어쩔 수 없이 필요한 상황에서만 이용하기를 권

3. const 단언

특정 값을 const 타입으로 단언하면 마치 변수를 const로 선언한 것 과 비슷하게 타입이 변경

let num4 = 10 as const;
// 10 Number Literal 타입으로 단언됨

let cat = {
  name: "야옹이",
  color: "yellow",
} as const;
// 모든 프로퍼티가 readonly를 갖도록 단언됨

4. Non Null 단언

값 뒤에 느낌표(!) 를 붙여주면 이 값이 undefined이거나 null이 아닐것으로 단언 Non Null 단언은 지금까지 살펴본 값 as 타입 형태를 따르지 않는 단언

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

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

const len: number = post.author!.length;

타입 좁히기

1. typeof 타입가드

function func(value: number | string) { }

function func(value: number | string) {
  value.toFixed() // 오류
	value.toUpperCase() // 오류
}
  • 매개변수 value의 타입이 number | string 이므로 함수 내부에서 다음과 같이 value가 number 타입이거나 string 타입일 것으로 기대하고 메서드를 사용하려고 하면 오류가 발생합니다.
  • 만약 value가 number 타입일거라고 기대하고 toFixed 메서드를 사용하고 싶다면 다음과 같이 조건문을 이용해 value의 타입이 number 타입임을 보장해줘야 한다.
function func(value: number | string) {

	// 타입 가드(if (typeof === ...))로 타입 좁히기
  if (typeof value === "number") {
    console.log(value.toFixed());
  }
}

2. instanceof 타입가드

내장 클래스 타입을 보장할 수 있는 타입가드를 만들 수 있다.

function func(value: number | string | Date | null) {
  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());
  }
}
  • 그러나 Instanceof는 내장 클래스 또는 직접 만든 클래스에만 사용이 가능한 연산이다. (따라서 우리가 직접 만든 타입과 함께 사용할 수 없음)

3. in 타입 가드

직접 만든 타입과 함께 사용하려면 다음과 같이 in 연산자를 이용

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

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}살 입니다`)

서로소 유니온 타입

서로소 유니온 타입서로소 유니온 타입은 교집합이 없는 타입들 즉 서로소 관계에 있는 타입들을 모아 만든 유니온 타입

// 적용 전

type Admin = {
  name: string;
  kickCount: number;
};

type Member = {
  name: string;
  point: number;
};

type Guest = {
  name: string;
  visitCount: number;
};

type User = Admin | Member | Guest;

function login(user: User) {
  if ("kickCount" in user) {
		// Admin
    console.log(`${user.name}님 현재까지 ${user.kickCount}명 추방했습니다`);
  } else if ("point" in user) {
		// Member
    console.log(`${user.name}님 현재까지 ${user.point}모았습니다`);
  } else {
		// Guest
    console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다`);
  }
}
  • 이렇게 코드를 작성하면 결과적으로 직관적이지 못한 코드
  • 이럴 때에는 다음과 같이 각 타입에 태그 프로퍼티를 추가 정의
// 적용 후 1.

type Admin = {
  tag: "ADMIN";
  name: string;
  kickCount: number;
};

type Member = {
  tag: "MEMBER";
  name: string;
  point: number;
};

type Guest = {
  tag: "GUEST";
  name: string;
  visitCount: number;
};

function login(user: User) {
  if (user.tag === "ADMIN") {
    console.log(`${user.name}님 현재까지 ${user.kickCount}명 추방했습니다`);
  } else if (user.tag === "MEMBER") {
    console.log(`${user.name}님 현재까지 ${user.point}모았습니다`);
  } else {
    console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다`);
  }
}
  • switch를 이용해 더 직관적으로 변경
function login(user: User) {
  switch (user.tag) {
    case "ADMIN": {
      console.log(`${user.name}님 현재까지 ${user.kickCount}명 추방했습니다`);
      break;
    }
    case "MEMBER": {
      console.log(`${user.name}님 현재까지 ${user.point}모았습니다`);
      break;
    }
    case "GUEST": {
      console.log(`${user.name}님 현재까지 ${user.visitCount}번 오셨습니다`);
      break;
    }
  }
}