본문 바로가기
Typescript

Typescript | 타입 추론, 타입 단언, 타입 좁히기, 서로소 유니온 타입

by 리잼 2024. 10. 29.
반응형

타입추론

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

이런 기능을 타입 추론 이라고 한다.

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

 

그러나 모든 상황에 타입을 추론하진 않는다.

예를 들어 함수의 매개변수 타입은 자동으로 추론할 수 없다.

function func(param){ // 오류

}
  • 이런 경우 암시적으로 any 타입이 추론된다.
  • 그러나 엄격한 타입 검사모드 ( tsconfig.json - strict 옵션 true ) 에서는 암시적 any 타입의 추론을 오류로 판단.

타입 추론이 가능한 상황

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";
}

 

주의해야할 상황들

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

  • 변수를 선언할때 초기값을 생략하면 암시적인 any 타입으로 추론된다.
  • 이때 매개변수의 타입이 암시적 any로 추론될 때와 달리 일반 변수의 타입이 암시적 any 타입으로 추론되는 상황은
    오류로 판단하지 않는다.
let d; // 암시적인 any 타입 추론
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 로 선언된 상수도 타입 추론이 진행된다. 그러나 let 으로 선언한 변수와는 다른 방식으로 추론된다.

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

const str = "hello";
// "hello" String Literal 타입으로 추론
  • 상수는 초기화 때 설정한 값을 변경할 수 없기 때문에 특별히 가장 좁은 타입으로 추론된다.

최적 공동 타입 ( 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 타입이라고 타입스크립트에게 단언해주면 된다.
(...)

let person = {} as Person;
person.name = "";
person.age = 23;
 
  • 이렇듯 값 as 타입 으로 특정 값을 원하는 타입으로 단언할 수 있습니다. 이를 타입 단언 이라고 함.

타입 단언은 다음과 같이 초과 프로퍼티 검사를 피할때에도 요긴하게 사용할 수 있다.

type Dog = {
  name: string;
  color: string;
};

let dog: Dog = {
  name: "돌돌이",
  color: "brown",
  breed: "진도",
} as Dog
  • 위 코드에서는 breed 라는 초과 프로퍼티가 존재하지만 이 값을 Dog 타입으로 단언하여 초과 프로퍼티 검사를 피했다.

타입 단언의 조건

타입 단언에도 조건이 있다.

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

  • A 가 B 의 슈퍼 타입이다.
  • A 가 B 의 서브 타입이다.

Ex

let num1 = 10 as never;   // ✅
let num2 = 10 as unknown; // ✅

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

다중단언

타입 단언은 다중으로도 가능하다.

다중 단언의 경우 왼쪽에서 오른쪽으로 단언이 이루어 진다.

let num3 = 10 as unknown as string;
  • number 타입의 값을 unknown 타입으로 단언
  • unknown 타입의 값을 string 타입으로 단언

이렇듯 중간에 값을 unknown 타입으로 단언하면 unknown 타입은 모든 타입의 슈퍼타입이므로
모든 타입으로 또 다시 단언하는게 가능하다.

그치만 이 방법은 매우 좋지 않으므로 어쩔 수 없는 경우에만 사용해야한다.

const 단언

타입 단언때에만 사용할 수 있는 const 타입이 존재한다.

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

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

let cat = {
  name: "야옹이",
  color: "yellow",
} as const;
// 모든 프로퍼티가 readonly를 갖도록 단언됨
  • 근데 이럴거면 그냥 const 로 선언하면 되지 않나..? 의문 

Not Null 단언

값 뒤에 ! 를 붙이면 undefined 이거나, null 이 아닐 것으로 단언

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

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

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

타입 좁히기

다음과 같은 함수가 있다고 가정

function func(value: number | string) { }
  • 이때 매개변수 value의 타입이 number | string 이므로 함수 내부에서 다음과 같이 value가 number 타입이거나
    string 타입일 것으로 기대하고 메서드를 사용하려고 하면 오류가 발생함.
function func(value: number | string) {
  value.toFixed() // 오류
	value.toUpperCase() // 오류
}
  • 만약 value가 number 타입일거라고 기대하고 toFixed 메서드를 사용하고 싶다면
    다음과 같이 조건문을 이용해 value의 타입이 number 타입임을 보장해줘야 한다.
function func(value: number | string) {
  if (typeof value === "number") {
    console.log(value.toFixed());
  }
}
  • 또 똑같이 value가 string 타입일거라고 기대하고 toUpperCase 메서드를 사용하고 싶다면
    다음과 같이 조건문을 이용해 value의 타입이 string 타입임을 보장해 주어야 한다.
function func(value: number | string) {
  if (typeof value === "number") {
    console.log(value.toFixed());
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());
  }
}
  • 이렇게 조건문을 이용해 조건문 내부에서 변수가 특정 타입임을 보장하면
    해당 조건문 내부에서는 변수의 타입이 보장된 타입으로 좁혀진다.
  • if ( typeof === 변수 ) 처럼 조건문과 함께 사용해 타입을 좁히는 표현들을 "타입 가드" 라고 부른다.

instanceof 타입가드

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 는 내장 클래스 또는 직접 만든 클래스에만 사용 가능한 연산이다.
    따라서 직접 만든 타입과는 함께 사용이 불가능하다.

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}번 오셨습니다`);
  }
}
  • 회원의 역할 분류에 따라 3개의 타입을 각각 정의 후 3개의 타입의 합집합 타입인 User 타입도 생성
  • login 함수는 User 타입의 매개변수 user를 받아 회원의 역할에 따라 각각 다른 기능을 수행하도록 함.
    자세한 동작은 다음과 같다.
    1. 첫번째 조건문이 참이되면 user에 kickCount 프로퍼티가 있으므로 이 유저는 Admin 타입으로 좁혀집니다.
    2. 두번째 조건문이 참이되면 user에 point 프로퍼티가 있으므로 이 유저는 Member 타입으로 좁혀집니다.
    3. 세번째 else 문까지 오면 user는 남은 타입인 Guest 타입으로 좁혀집니다.
그러나 이렇게 코드를 작성하면 조건식만 보고 어떤 타입으로 좁혀지는지 바로 파악하기가 좀 어렵다.
결과적으로 직관적이지 못한 코드이다.
이럴 때에는 다음과 같이 각 타입에 태그 프로퍼티를 추가 정의해주면 된다.
type Admin = {
  tag: "ADMIN";
  name: string;
  kickCount: number;
};

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

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

(...)
  • Admin 타입에는 “ADMIN” String Literal 타입의 tag 프로퍼티
  • Member 타입에는 “MEMBER” String Literal 타입의 tag 프로퍼티
  • Guest 타입에는 “GUEST” String Literal 타입의 tag 프로퍼티를 각각 추가
  • tag 프로퍼티를 넣으므로서 login 함수의 타입가드를 다음과 같이 더 직관적으로 수정할 수 있게 된다.
(...)

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;
    }
  }
}
반응형