타입추론
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를 받아 회원의 역할에 따라 각각 다른 기능을 수행하도록 함.
자세한 동작은 다음과 같다.- 첫번째 조건문이 참이되면 user에 kickCount 프로퍼티가 있으므로 이 유저는 Admin 타입으로 좁혀집니다.
- 두번째 조건문이 참이되면 user에 point 프로퍼티가 있으므로 이 유저는 Member 타입으로 좁혀집니다.
- 세번째 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;
}
}
}
'Typescript' 카테고리의 다른 글
Typescript | 함수 오버로딩, 사용자 정의 타입가드 (0) | 2024.11.02 |
---|---|
Typescript | 함수 타입, 함수 타입 표현식과 호출 시그니처, 호환성 (0) | 2024.11.02 |
Typescript | 타입 호환성, 대수타입 (2) | 2024.10.29 |
Typescript | void, never (0) | 2024.10.25 |
Typescript | Enum, any, unknown (0) | 2024.10.25 |