01
객체 타입 호환성 (Object Type Compatibility)
객체 타입 호환성은 간단히 말해서, 두 객체가 비슷한 구조일 때 서로 바꿔 사용할 수 있다는 개념입니다.
객체의 속성 이름과 타입이 같으면, 두 객체를 서로 호환된다고 보는 거죠.
예를 들어, name과 price 속성이 있는 객체가 있다면, 이 두 객체는 서로 호환된다고 판단합니다.
객체의 구조가 맞다면 타입이 맞다고 간주되기 때문에, 쉽게 값을 교환할 수 있게 되죠.
type Book {
name: string;
price: number;
}
let myBook: Book = {
name: "Learning TypeScript",
price: 30000,
author: "John Doe" // Error: 타입에 정의되지 않은 속성
};
- 여기서 author 속성은 Book 타입에 정의되지 않았으므로, 타입스크립트는 에러를 발생시킵니다.
객체 리터럴에 불필요한 속성을 넣을 수 없다는 거죠.
✅ 초과 프로퍼티 검사 피하기
이 문제를 피하려면, 객체 리터럴을 사용하더라도 타입을 명확하게 지정하는 방법을 사용할 수 있습니다.
let programingBook: Book = { name: "TypeScript", price: 40000 };
// 타입이 맞는 경우:
let myBook2: Book = programingBook; // 올바른 타입 호환성
혹은 함수에서 객체를 넘길 때도 타입을 명확히 지정하여, 초과 프로퍼티를 방지할 수 있습니다.
function func(book: Book) {
console.log(book);
}
func({
name: "한입",
price: 33000 // 초과 프로퍼티 없이 올바른 타입
});
02
타입을 결합하는 방식
1. 대수 타입 (Union Types)
대수 타입은 말 그대로 여러 타입을 합쳐서 하나의 타입을 만든다는 개념이에요.
이때 | 기호를 사용해 여러 타입을 합치죠. 그래서 이걸 유니온 타입 (Union Types)이라고도 부릅니다.
let value: string | number; // value는 string 또는 number
value = "Hello"; // 괜찮아요, value가 string 타입
value = 42; // 괜찮아요, value가 number 타입
- 대수 타입은 하나의 변수가 여러 타입 중 하나를 가질 수 있도록 허용하는 타입입니다.
2. 교집합 타입 (Intersection Types)
교집합 타입은 두 개 이상의 타입을 합쳐서 그 타입들이 모두 갖고 있는 속성을 가지는 새로운 타입을 만드는 방식입니다. 이때는 & 기호를 사용하죠.
type Dog = { name: string; age: number };
type Person = { name: string; language: string };
type DogPerson = Dog & Person;
let dogPerson: DogPerson = {
name: "Buddy", // Dog와 Person 모두 name 속성 필요
age: 5, // Dog에 있는 age 속성 필요
language: "English" // Person에 있는 language 속성 필요
};
- 교집합 타입은 두 개 이상의 타입이 모두 갖고 있는 속성을 합쳐서 만든 타입입니다.
3. 대수 타입과 교집합 타입의 차이
✅ 대수 타입 (Union Types)
: 하나의 변수는 여러 타입 중 하나를 가질 수 있습니다. string | number처럼 | 기호로 합성한 타입입니다.
✅ 교집합 타입 (Intersection Types)
: 하나의 변수는 여러 타입의 속성 모두를 가질 수 있습니다. Dog & Person처럼 & 기호로 합성한 타입입니다.
03
타입 추론 (Type Inference)
타입 추론은 TypeScript가 자동으로 변수의 타입을 추측해서 지정해 주는 기능이에요.
우리가 타입을 명시하지 않아도, TypeScript가 할당된 값을 보고 변수의 타입을 자동으로 추론합니다.
이는 코드 작성 시 더 간결하게 만들어주고, 타입에 대한 실수를 줄여줘요.
하지만, 모든 상황에서 타입을 추론해 주는 것은 아니에요.
특정 상황에서는 TypeScript가 any 타입으로 추론할 수 있기 때문에 주의가 필요합니다.
1. 타입 추론이 적용되는 경우
✅ 변수를 선언하고 초기화할 때 TypeScript는 변수에 초기값을 기준으로 타입을 추론합니다.
let num = 10; // TypeScript는 num을 number 타입으로 추론
let text = "Hello"; // TypeScript는 text를 string 타입으로 추론
✅ 객체를 구조 분해할 때 객체의 속성도 추론을 통해 타입을 지정해줍니다.
const person = { name: "Alice", age: 30 };
const { name, age } = person; // name은 string, age는 number 타입으로 추론
✅ 함수의 반환값 타입 TypeScript는 함수의 반환값을 보고 타입을 추론합니다.
함수에 초기값이 아닌 반환값을 기준으로 타입을 추론해요.
function add(a: number, b: number) {
return a + b; // 반환값이 number로 추론됨
}
✅ 매개변수 기본값을 기준으로 타입 추론 함수 매개변수의 기본값을 기준으로 타입을 추론할 수 있습니다.
function greet(name = "Guest") {
console.log(name); // name은 string으로 추론됨
}
✅ const로 선언할 때 const로 선언한 변수는 리터럴 타입으로 추론됩니다.
이는 값이 변하지 않기 때문에 값 그대로 타입이 고정됩니다.
const num = 10; // num은 10이라는 값으로 리터럴 타입(number)으로 추론
const str = "hello"; // str은 "hello"라는 값으로 리터럴 타입(string)으로 추론
✅ 배열 배열도 타입 추론이 가능합니다. 배열의 각 요소 타입에 맞게 유니온 타입으로 추론될 수 있습니다.
let arr = [1, "hello"]; // number | string 타입의 유니온 타입으로 추론
2. 타입 추론이 당황스러운 경우
✅ 초기값이 없을 때 변수를 선언하고 초기값을 주지 않으면 TypeScript는 any 타입으로 추론합니다.
any 타입은 어떤 타입도 될 수 있는 타입이라 안전하지 않아요.
let a; // TypeScript는 a를 any 타입으로 추론
a = 10; // a는 number 타입으로 추론됨
a.toFixed(); // 문제 없음, number 타입이므로 호출 가능
a = "hello"; // a는 string 타입으로 변환됨
- 이처럼 a의 타입이 계속 변하는 현상은 any 타입의 진화라고 합니다.
any 타입으로 시작해서 타입이 변해가는 과정이죠.
✅ 암묵적 any 타입 TypeScript가 타입을 추론할 수 없을 때, 암묵적으로 any 타입을 사용하게 됩니다.
이는 초기값이 없는 변수에서 발생합니다.
let a; // 암묵적으로 any 타입
a = 10; // number 타입으로 진화
a = "hello"; // string 타입으로 변함
04
타입 넓히기 (Type Widening)
타입 넓히기는 TypeScript가 변수의 타입을 좀 더 범용적으로 확장하는 과정입니다.
예를 들어, 리터럴 타입(숫자나 문자열 등)을 사용하여 변수의 타입을 추론할 때,
TypeScript는 그 값을 너무 제한적이지 않게 넓은 타입으로 추론합니다.
이렇게 함으로써 프로그래머가 변수를 더 유연하게 사용할 수 있도록 돕습니다.
✅ let 변수 타입 넓히기
let a = 10; // a는 number 타입으로 추론
a = 20; // 정상: number 타입으로 넓혀졌기 때문에 숫자 할당 가능
a = 30; // 정상: 여전히 number 타입
✅ const 변수와 리터럴 타입
const b = 10; // b는 10이라는 리터럴 타입으로 고정
// b = 20; // 오류: const로 고정된 값이므로 변경할 수 없음
✅ 문자열 타입 넓히기
let name = "Alice"; // name은 string 타입으로 추론
name = "Bob"; // 정상: string 타입으로 넓혀졌기 때문에 다른 문자열 할당 가능
name = "Charlie"; // 정상
05
타입 단언 (Type Assertion)
타입 단언은 TypeScript에게 "이 변수는 내가 지정한 타입이다!"라고 확신을 주는 방법입니다.
타입을 변경하는 것이 아니라, TypeScript의 눈을 가리고 그 타입을 "그대로 믿고" 처리하게 만드는 방식입니다.
하지만, 타입 단언은 조심해서 사용해야 합니다. 왜냐하면 타입스크립트는 더 이상 타입 체크를 하지 않고, 우리가 강제로 지정한 타입을 신뢰하기 때문입니다. 따라서 타입을 확실히 알 때만 사용하는 것이 좋습니다.
type Animal = {
name: string;
age: number;
};
let dog = {} as Animal; // dog는 Animal 타입으로 단언
📂설명
- dog는 빈 객체 {}로 시작합니다. TypeScript는 이 객체의 타입을 알 수 없기 때문에 dog의 타입을 any로 추론합니다.
- 그러나 우리가 as Animal을 사용해서 dog의 타입을 Animal로 강제로 지정합니다.
이는 TypeScript에게 "이 변수는 반드시 Animal 타입이야!"라고 확신을 주는 방식입니다.
1. 타입 단언의 규칙
✅ 타입 단언은 확실할 때만 사용
: 타입 단언은 값을 강제로 특정 타입으로 지정하는 것이므로, 타입을 확신할 수 있을 때만 사용해야 합니다.
let num1 = 10 as never; // 오류: `never`로 단언 불가능
let num2 = 10 as string; // 오류: `number`를 `string`으로 단언할 수 없음
위처럼 타입 간에 충돌이 있을 경우 타입 단언은 허용되지 않습니다.
잘못된 타입으로 단언하면 런타임 오류가 발생할 수 있습니다.
✅ 서브타입과 슈퍼타입 관계
: 타입 단언을 사용할 때 타입 간에 겹치는 부분이 있어야 합니다. 슈퍼타입과 서브타입 관계가 성립해야 합니다.
let num3 = 10 as unknown; // 정상: `unknown`은 모든 타입을 포함
let num4 = 10 as string; // 오류: `number`는 `string`으로 단언 불가
unknown은 모든 타입을 포함하므로 단언이 가능하지만, number는 **string**과 호환되지 않아 오류가 발생합니다.
❌ 다중 타입 단언 (Cascading Assertion)
: 다중 단언을 사용하면 타입이 강제로 변환될 수 있습니다. 하지만 이 방식은 비추천입니다
let num5 = 10 as unknown as string; // 가능하지만 비추천
2. 특별한 타입 단언
✅ const 단언
: as const를 사용하면 값이 리터럴 타입으로 고정되며, readonly로 처리됩니다.
let num = 10 as const; // num은 리터럴 10 타입으로 고정
let cat = { name: "야옹이", color: "yellow" } as const; // cat은 readonly로 고정
as const를 사용하면 읽기 전용으로 고정되며, 값이 변경되지 않도록 합니다.
✅ Non-null 단언 (!)
: 옵셔널 체이닝을 사용할 때, null이나 undefined가 아님을 확신할 때 Non-null 단언을 사용합니다.
type Post = { title: string; author?: string };
let post: Post = { title: "게시글1", author: "아무개" };
const len: number = post.author!.length; // author는 확실히 존재한다고 단언
Non-null 단언 (!)을 사용하면 undefined나 null이 아닌 값을 확실히 다룰 수 있게 됩니다.
06
타입 좁히기 (Type Narrowing)
타입 좁히기는 조건문이나 타입 체크를 통해 타입을 더 구체적으로 좁히는 방법입니다.
예를 들어, 어떤 변수가 여러 타입 중 하나일 때, 조건을 통해 이 변수가 어떤 타입일지 좁혀나가는 방식이죠.
1. 타입 가드
타입 가드는 타입 좁히기를 보다 명확하게 할 수 있는 방법입니다.
typeof, instanceof, in 연산자와 같은 조건문을 사용하여 타입 좁히기를 수행할 때, 이를 타입 가드라고 부릅니다.
따라서 타입 가드는 타입 좁히기를 실현하는 도구로, 타입 좁히기를 쉽게 구현할 수 있도록 도와주는 역할을 합니다.
✅ typeof를 이용한 타입 좁히기
typeof는 원시 타입(string, number, boolean 등)을 확인할 때 사용합니다.
예를 들어, string과 number를 구분할 때 유용합니다.
function printLength(value: string | number) {
if (typeof value === "string") {
console.log(value.length); // value는 string 타입으로 좁혀짐
} else {
console.log(value.toFixed(2)); // value는 number 타입으로 좁혀짐
}
}
printLength("Hello"); // 5
printLength(123.456); // 123.46
✅ instanceof를 이용한 타입 좁히기
instanceof는 클래스 인스턴스인지 확인할 때 사용됩니다.
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark(); // animal은 Dog 타입으로 좁혀짐
} else {
animal.meow(); // animal은 Cat 타입으로 좁혀짐
}
}
const dog = new Dog();
const cat = new Cat();
makeSound(dog); // Woof!
makeSound(cat); // Meow!
- animal instanceof Dog를 사용해서 animal이 Dog 타입일 때는 bark 메서드를,
Cat 타입일 때는 meow 메서드를 호출합니다.
✅ in 연산자를 이용한 타입 좁히기
in 연산자는 객체가 특정 속성을 가지고 있는지 확인할 때 사용합니다.
null이나 undefined가 포함되면 안 되므로 &&를 사용해서 체크해야 합니다.
type Dog = { type: "dog"; bark: () => void };
type Cat = { type: "cat"; meow: () => void };
function makeSound(animal: Dog | Cat | null | undefined) {
if (animal && "bark" in animal) {
// animal이 null이 아니고, "bark" 속성이 있으면 Dog 타입
animal.bark(); // 정상
} else if (animal) {
// animal이 null이 아니고, "bark" 속성이 없으면 Cat 타입
animal.meow(); // 정상
} else {
console.log("No animal provided");
}
}
const dog: Dog = { type: "dog", bark: () => console.log("Woof!") };
const cat: Cat = { type: "cat", meow: () => console.log("Meow!") };
makeSound(dog); // Woof!
makeSound(cat); // Meow!
makeSound(null); // No animal provided
makeSound(undefined); // No animal provided
- animal && "bark" in animal을 사용해서 animal이 null 또는 undefined가 아닌지 먼저 확인하고,
이후 bark 속성의 존재 여부로 타입을 좁힙니다.
✅ 사용자 정의 타입 가드
타입 가드를 사용자 정의 함수로 만들어, 더 정확한 타입 좁히기를 할 수도 있습니다.
type Dog = { type: "dog"; bark: () => void };
type Cat = { type: "cat"; meow: () => void };
function isDog(animal: Dog | Cat): animal is Dog {
return animal.type === "dog";
}
function makeSound(animal: Dog | Cat) {
if (isDog(animal)) {
animal.bark(); // animal은 Dog 타입으로 좁혀짐
} else {
animal.meow(); // animal은 Cat 타입으로 좁혀짐
}
}
isDog 함수는 타입 가드로 animal이 Dog 타입인지 체크하고, 타입을 좁히는 역할을 합니다.
07
서로소 유니온 타입 (Disjoint Union Types)
서로소 유니온 타입은 서로 겹치지 않는 유니온 타입을 의미해요.
즉, 하나의 변수가 여러 타입을 가질 수 있지만, 그 타입들은 서로 겹치지 않도록 정의되는 방식입니다.
주로 타입 간 구분이 명확한 경우에 사용됩니다.
예를 들어, 사람과 동물이라는 두 타입이 있을 때, 사람은 사람 타입만, 동물은 동물 타입만 가질 수 있습니다.
이때 사람과 동물은 서로 겹치지 않기 때문에 서로소 유니온 타입이라고 할 수 있습니다.
1. 서로소 유니온 타입의 사용
서로소 유니온 타입은 주로 타입 간 구분이 명확할 때 사용됩니다.
예를 들어, 두 타입이 서로 겹치지 않고 완전히 다른 경우에 사용하면 좋습니다.
✅ if 조건문 활용
type Person = { type: "person"; name: string };
type Animal = { type: "animal"; species: string };
type HumanOrAnimal = Person | Animal;
function describe(entity: HumanOrAnimal) {
if (entity.type === "person") {
console.log(`Person: ${entity.name}`);
} else {
console.log(`Animal: ${entity.species}`);
}
}
const person: Person = { type: "person", name: "John" };
const animal: Animal = { type: "animal", species: "Dog" };
describe(person); // Person: John
describe(animal); // Animal: Dog
- Person과 Animal 타입은 서로소 유니온 타입입니다.
즉, type 값이 "person"인 객체는 사람이고, "animal"인 객체는 동물이므로 겹치는 부분이 없습니다.
✅ switch 조건문 활용
type Success = { status: "success"; data: string };
type Failure = { status: "failure"; error: string };
type Response = Success | Failure;
function handleResponse(response: Response) {
switch (response.status) {
case "success":
console.log(`Data: ${response.data}`);
break;
case "failure":
console.log(`Error: ${response.error}`);
break;
default:
console.log("Unknown status");
}
}
const successResponse: Success = { status: "success", data: "All good!" };
const failureResponse: Failure = { status: "failure", error: "Something went wrong" };
handleResponse(successResponse); // Data: All good!
handleResponse(failureResponse); // Error: Something went wrong
- switch문에서 response.status를 체크하여, success일 때와 failure일 때를 분기합니다.
2. 서로소 유니온 타입이 유용한 상황
서로소 유니온 타입은 타입 간 구분이 확실한 경우에 유용합니다.
예를 들어, 두 개 이상의 타입이 겹치지 않고 각기 다른 값을 가질 때, 서로소 유니온을 사용하면 코드가 더 명확하고 안전해집니다.
type Success = { status: "success"; data: string };
type Failure = { status: "failure"; error: string };
type Response = Success | Failure;
function handleResponse(response: Response) {
if (response.status === "success") {
console.log(`Data: ${response.data}`);
} else {
console.log(`Error: ${response.error}`);
}
}
const successResponse: Success = { status: "success", data: "All good!" };
const failureResponse: Failure = { status: "failure", error: "Something went wrong" };
handleResponse(successResponse); // Data: All good!
handleResponse(failureResponse); // Error: Something went wrong
- Success와 Failure는 서로소 유니온 타입입니다.
두 타입은 서로 겹치지 않기 때문에 status 값으로 구분이 가능하며, 타입 좁히기를 할 때 유용합니다.
08
마무리
타입스크립트를 배우는 과정에서 여러 개념이 조금 헷갈릴 수 있지만, 각 개념을 하나하나 이해하고 나면 훨씬 더 효율적이고 안전한 코드를 작성할 수 있습니다.
특히 서로소 유니온 타입처럼 타입이 명확하게 구분될 때 사용하면, 코드가 더 직관적이고 유지보수하기 좋습니다.
저는 몇 번이고 좀 더 읽어봐야 이해를 할 것 같네요 😂
타입 | 설명 |
객체 타입 호환성 | 객체의 속성 이름과 타입이 동일하면 객체 타입이 호환되어 교환 가능 |
타입 결합 |
|
타입 추론 | TypeScript는 변수에 할당된 값을 보고 자동으로 타입을 추론 |
타입 넓히기 | 리터럴 타입을 넓혀서 변수 타입을 범용적으로 확장 |
타입 단언 | TypeScript에 "이 변수는 이 타입이야!"라고 확신을 주는 방식, 확실한 경우에만 사용 |
타입 좁히기 | 조건문 등을 사용해 변수를 더 구체적인 타입으로 좁히는 방식 |
서로소 유니온 타입 | 겹치지 않는 타입들을 결합하여 코드의 안전성을 높임 |
'DEVELOPMENT > Typescript' 카테고리의 다른 글
[TS] 인터페이스 (Interface) (0) | 2025.02.28 |
---|---|
[TS] 함수와 타입 (1) | 2025.02.27 |
[TS] 타입 계층과 변환 개념 (업캐스팅, 다운캐스팅, unknown, never) (0) | 2025.02.25 |
[TS] 타입스크립트 모든 타입 한 번에 정리 (0) | 2025.02.25 |
[TS] "Hello TypeScript World!" (1) | 2025.02.25 |