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; // ❌
- num1은 A(number 타입)의 값을 B(never) 타입으로 단언 → never 타입은 모든 타입의 서브타입이므로 A가 B의 슈퍼타입
- num2는 A(number 타입)의 값을 B(unknown) 타입으로 단언 → unknown 타입은 모든 타입의 슈퍼타입이므로 A가 B의 서브타입
- 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;
}
}
}