TypeScript / / 2024. 10. 25. 07:38

TypeScript 정리2 (이해하기)

1. object 타입 정의의 문제점

자바스크립트에서 객체는 자유롭게 다양한 프로퍼티를 가질 수 있고, 타입스크립트에서도 object 타입을 사용할 수 있습니다. 하지만 object 타입은 객체임을 표현하는 것 외에 객체의 프로퍼티에 대한 정보를 제공하지 않는 타입입니다.

예시:

let user: object = {
  id: 1,
  name: "김수현",
};

// user.id;  // 오류 발생: 'object' 타입에는 'id' 프로퍼티가 없습니다

이 경우 user 변수를 object 타입으로 정의했지만, **id**와 같은 특정 프로퍼티에 접근하려고 하면 오류가 발생합니다. 이는 object 타입이 객체의 구조에 대한 정보가 없기 때문입니다.

2. 객체 리터럴 타입으로 타입 정의하기

객체의 구조를 명확하게 타입으로 정의하려면 객체 리터럴 타입을 사용해야 합니다. 객체 리터럴 타입을 사용하면 객체의 프로퍼티와 타입을 명확히 정의할 수 있습니다.

예시:

let user: {
  id: number;
  name: string;
} = {
  id: 1,
  name: "김수현",
};

console.log(user.id);  // 정상 출력: 1

이렇게 객체 리터럴 타입을 사용하면 객체의 프로퍼티에 안전하게 접근할 수 있으며, 컴파일러는 각 프로퍼티의 타입을 명확히 알 수 있습니다.

3. 선택적 프로퍼티 (Optional Property)

어떤 객체에서는 특정 프로퍼티가 선택적으로 존재할 수 있습니다. 타입스크립트에서는 선택적 프로퍼티를 정의할 때, 프로퍼티 이름 뒤에 **?**를 붙여서 이를 표현할 수 있습니다.

예시:

let user: {
  id?: number;  // 선택적 프로퍼티
  name: string;
} = {
  name: "홍길동",
};

console.log(user.id);  // undefined, id는 선택적으로 존재

id 프로퍼티는 선택적이므로 생략될 수 있으며, user 객체에 **name**만 있어도 타입 체크에 통과합니다.

4. 읽기 전용 프로퍼티 (Readonly Property)

객체의 특정 프로퍼티가 변경되지 않도록 하고 싶을 때는 readonly 키워드를 사용해 읽기 전용으로 만들 수 있습니다.

예시:

let user: {
  id?: number;
  readonly name: string;  // 읽기 전용 프로퍼티
} = {
  id: 1,
  name: "김수현",
};

// user.name = "홍길동";  // 오류 발생: 'name'은 읽기 전용입니다.

name 프로퍼티는 읽기 전용이기 때문에 객체가 생성된 이후에는 값을 변경할 수 없습니다. 이를 통해 프로퍼티가 의도치 않게 변경되는 것을 방지할 수 있습니다.

통합 예시 코드

위에서 설명한 내용을 모두 통합한 하나의 예시 코드는 다음과 같습니다:

let user: {
  id?: number;          // 선택적 프로퍼티
  readonly name: string;  // 읽기 전용 프로퍼티
} = {
  id: 1,
  name: "김수현",
};

// 정상적인 프로퍼티 접근
console.log(user.name);  // 출력: "김수현"

// id는 선택적이므로 존재하지 않을 수도 있음
user = {
  name: "홍길동"
};

console.log(user.id);  // 출력: undefined

// 읽기 전용 프로퍼티는 수정 불가
// user.name = "홍길동";  // 오류 발생: 'name'은 읽기 전용입니다.

요약

  • object 타입: 객체임을 나타내지만, 프로퍼티 정보가 없어 프로퍼티 접근이 제한됨.
  • 객체 리터럴 타입: 프로퍼티와 타입을 명시적으로 정의하여 프로퍼티 접근 가능.
  • 선택적 프로퍼티: 특정 프로퍼티가 없어도 되는 경우, **?**를 붙여 선택적으로 만듦.
  • 읽기 전용 프로퍼티: **readonly**를 사용하여 프로퍼티의 값을 수정하지 못하도록 설정.

이 방식으로 객체의 타입을 명확하게 정의하면, 코드가 더 안전해지고 유지보수가 쉬워집니다.


타입 별칭 (Type Alias)

타입 별칭은 객체 타입을 정의할 때 반복적인 타입 정의를 줄이고, 코드 가독성을 높이는 방법입니다. 별칭을 정의하면 그 이름을 사용해 해당 타입을 재사용할 수 있습니다.

타입 별칭 정의 예시:

// 타입 별칭 정의
type User = {
  id: number;
  name: string;
  nickname: string;
  birth: string;
  bio: string;
  location: string;
};

// 타입 별칭을 사용하여 객체 타입 정의
let user: User = {
  id: 1,
  name: "김수현",
  nickname: "winterlood",
  birth: "1997.01.07",
  bio: "안녕하세요",
  location: "부천시",
};

let user2: User = {
  id: 2,
  name: "홍길동",
  nickname: "winterlood",
  birth: "1997.01.07",
  bio: "안녕하세요",
  location: "서울시",
};

여기서 **User**라는 타입 별칭을 정의한 후, 그 타입을 사용하여 **user**와 user2 객체의 타입을 지정했습니다. 타입 별칭을 사용하면 같은 객체 구조를 반복적으로 정의하지 않고도 동일한 타입을 여러 변수에 적용할 수 있습니다.

참고: 스코프 내 중복된 타입 별칭 정의 불가

동일한 스코프 내에서는 같은 이름의 타입 별칭을 중복 정의할 수 없습니다.

type User = {
  id: number;
  name: string;
};

// 오류 발생! 동일 스코프 내에서 같은 이름으로 타입 별칭을 재정의할 수 없습니다.
type User = {
  nickname: string;
};

하지만 스코프가 다르다면 동일한 이름의 타입을 다시 정의할 수 있습니다.

type User = {
  id: number;
  name: string;
};

function test() {
  type User = string;  // test 함수 안에서는 User가 string 타입
}

2. 인덱스 시그니처 (Index Signature)

인덱스 시그니처는 객체의 프로퍼티 이름과 개수를 미리 알 수 없을 때 유용하게 사용됩니다. 즉, 동적으로 정의된 프로퍼티가 많은 경우, 인덱스 시그니처를 사용하여 객체의 타입을 유연하게 정의할 수 있습니다.

기본 인덱스 시그니처 예시:

다음은 국가 코드를 저장하는 객체를 인덱스 시그니처로 정의한 예입니다.

// 인덱스 시그니처: key는 string, 값은 string
type CountryCodes = {
  [key: string]: string;
};

let countryCodes: CountryCodes = {
  Korea: "ko",
  UnitedState: "us",
  UnitedKingdom: "uk",
  Brazil: "bz",
};

여기서 **[key: string]: string**은 프로퍼티 이름이 문자열이고, 그 값이 문자열 타입임을 나타냅니다. 즉, 객체에 key와 value로 문자열을 포함하는 모든 프로퍼티를 유연하게 추가할 수 있습니다.

숫자 타입을 가진 인덱스 시그니처 예시:

이번에는 국가 코드를 숫자로 저장할 경우 인덱스 시그니처를 정의하는 예입니다.

// 인덱스 시그니처: key는 string, 값은 number
type CountryNumberCodes = {
  [key: string]: number;
};

let countryNumberCodes: CountryNumberCodes = {
  Korea: 82,
  UnitedState: 1,
  UnitedKingdom: 44,
  Brazil: 55,
};

특정 프로퍼티와 함께 사용하는 인덱스 시그니처:

인덱스 시그니처를 사용하면서 특정 프로퍼티는 반드시 포함해야 한다면, 해당 프로퍼티를 명시적으로 정의할 수 있습니다. 다만, 인덱스 시그니처의 value 타입과 추가된 프로퍼티의 value 타입이 호환되어야 합니다.

// 인덱스 시그니처와 특정 프로퍼티 정의
type CountryNumberCodes = {
  [key: string]: number;  // 인덱스 시그니처
  Korea: number;  // 특정 프로퍼티
};

let countryNumberCodes: CountryNumberCodes = {
  Korea: 82,
  UnitedState: 1,
  UnitedKingdom: 44,
  Brazil: 55,
};

주의: 타입 호환 문제

인덱스 시그니처를 사용하면서 특정 프로퍼티와의 타입이 호환되지 않으면 오류가 발생합니다.

type CountryNumberCodes = {
  [key: string]: number;
  Korea: string;  // 오류 발생! string 타입은 number 타입과 호환되지 않음
};

이 경우, Korea 프로퍼티의 타입은 인덱스 시그니처의 타입인 **number**와 호환되지 않으므로 오류가 발생합니다. 인덱스 시그니처와 특정 프로퍼티는 같은 타입이어야 합니다.

통합 예시 코드

위에서 설명한 내용을 모두 하나의 코드로 통합해보겠습니다.

// 타입 별칭 정의
type User = {
  id: number;
  name: string;
  nickname: string;
  birth: string;
  bio: string;
  location: string;
};

// 인덱스 시그니처 정의
type CountryCodes = {
  [key: string]: string;  // 인덱스 시그니처: 모든 key는 string, 값은 string
};

// User 타입을 사용한 객체
let user: User = {
  id: 1,
  name: "김수현",
  nickname: "winterlood",
  birth: "1997.01.07",
  bio: "안녕하세요",
  location: "부천시",
};

// CountryCodes 타입을 사용한 객체
let countryCodes: CountryCodes = {
  Korea: "ko",
  UnitedState: "us",
  UnitedKingdom: "uk",
  Brazil: "bz",
};

// 특정 프로퍼티가 있는 인덱스 시그니처 정의
type CountryNumberCodes = {
  [key: string]: number;
  Korea: number;  // 반드시 포함되어야 하는 프로퍼티
};

let countryNumberCodes: CountryNumberCodes = {
  Korea: 82,
  UnitedState: 1,
  UnitedKingdom: 44,
  Brazil: 55,
};

이 코드에서는 타입 별칭과 인덱스 시그니처를 사용하여 객체의 구조를 유연하고 명확하게 정의할 수 있습니다.


열거형 enum 타입

enum Role {
  ADMIN = 10,  // 10
  USER,        // 11
  GUEST        // 12
}

enum Language {
  Korean = "ko",
  English = "en",
  Japanese = "jp"
}

const user1 = {
  name: "이연",
  role: Role.ADMIN,  // 10
  language: Language.Korean  // "ko"
};

const user2 = {
  name: "홍길동",
  role: Role.USER,  // 11
  language: Language.English  // "en"
};

const user3 = {
  name: "아무개",
  role: Role.GUEST,  // 12
  language: Language.Japanese  // "jp"
};

console.log(user1.role);      // 출력: 10
console.log(user1.language);  // 출력: "ko"
console.log(user2.role);      // 출력: 11
console.log(user2.language);  // 출력: "en"
console.log(user3.role);      // 출력: 12
console.log(user3.language);  // 출력: "jp"

이 코드는 유저의 권한과 언어 설정을 **enum**으로 관리하는 예시입니다. Role 열거형은 숫자형 enum을 사용하고, Language 열거형은 문자열 enum을 사용하여 안전하고 직관적인 코드 작성을 가능하게 합니다.

요약

  • 숫자형 enum은 기본적으로 0부터 시작하며, 자동으로 1씩 증가된 값을 할당받을 수 있습니다.
  • 문자열 enum은 상수 문자열을 정의하고, 오류를 방지하기 위한 방법으로 유용합니다.
  • **enum**은 컴파일 후에도 객체로 변환되어 런타임에서 값을 사용할 수 있습니다.

**enum**을 사용하면 코드의 가독성과 타입 안전성을 높일 수 있으며, 실수로 잘못된 값을 사용하는 오류를 줄일 수 있습니다.


객체 타입의 호환성

타입스크립트에서는 객체 타입 간의 호환성을 비교할 때, **구조적 서브타입(subtyping)**을 따릅니다. 즉, 두 객체 타입이 형식적으로 일치하는지에 따라 타입 호환이 결정됩니다. 여기서 핵심은, 한 타입이 다른 타입보다 많은 프로퍼티를 가질 수 있지만, 적은 프로퍼티를 가지는 타입은 더 많은 프로퍼티를 요구하는 타입에 할당할 수 없다는 것입니다.

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

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

let animal: Animal = {
  name: "기린",
  color: "yellow",
};

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

animal = dog; // ✅ OK
dog = animal; // ❌ NO
  • Dog 타입은 Animal 타입을 포함합니다. 즉, **Dog**는 **Animal**에 추가적인 프로퍼티인 **breed**를 가지고 있지만, **name**과 **color**라는 공통된 프로퍼티도 포함하고 있습니다.
  • 호환성Dog 타입은 **Animal**의 모든 속성을 가지고 있기 때문에, Dog 객체를 Animal 타입의 변수에 할당할 수 있습니다. 이는 구조적으로 **Dog**가 **Animal**을 포함하므로 호환성이 인정되는 것입니다.
  • 호환 불가: 반대로, Animal 타입은 breed 프로퍼티가 없기 때문에 Dog 타입의 변수에 할당할 수 없습니다. 즉, 더 많은 프로퍼티를 요구하는 타입(Dog)에 더 적은 프로퍼티를 가진 타입(Animal)을 할당할 수 없습니다.

이것은 마치 상속 관계에서 슈퍼타입과 서브타입의 관계처럼 작동합니다. **Dog**가 **Animal**의 하위 타입(서브타입)이므로 Dog 객체는 **Animal**로서 작동할 수 있지만, 반대로 **Animal**은 **Dog**로서 작동할 수 없습니다.

또 다른 예시:

type Book = {
  name: string;
  price: number;
};

type ProgrammingBook = {
  name: string;
  price: number;
  skill: string;
};

let book: Book;
let programmingBook: ProgrammingBook = {
  name: "한 입 크기로 잘라먹는 리액트",
  price: 33000,
  skill: "reactjs",
};

book = programmingBook; // ✅ OK
programmingBook = book; // ❌ NO
  • 호환성ProgrammingBook 타입은 Book 타입의 모든 프로퍼티를 포함하고 있고, 추가로 **skill**이라는 프로퍼티를 가지기 때문에 **ProgrammingBook**은 **Book**에 할당할 수 있습니다. **book = programmingBook;**은 문제없이 작동합니다.
  • 호환 불가: 반대로 Book 타입은 **ProgrammingBook**이 요구하는 skill 프로퍼티가 없으므로, **programmingBook = book;**은 타입 불일치로 인해 오류가 발생합니다.

요약하자면, 더 많은 프로퍼티를 가진 객체는 적은 프로퍼티를 요구하는 타입에 할당할 수 있지만, 반대의 경우는 허용되지 않습니다.


초과 프로퍼티 검사

타입스크립트에서는 객체 리터럴을 할당할 때, 초과 프로퍼티 검사가 자동으로 이루어집니다. 이는 객체 리터럴이 타입 정의에 맞지 않는 추가적인 프로퍼티를 포함하는 경우, 타입 오류를 발생시켜주는 기능입니다. 이 기능은 객체의 구조를 정확히 정의하고, 실수를 방지하기 위해 매우 유용합니다.

예시:

type Book = {
  name: string;
  price: number;
};

type ProgrammingBook = {
  name: string;
  price: number;
  skill: string;
};

let book2: Book = {
  name: "한 입 크기로 잘라먹는 리액트",
  price: 33000,
  skill: "reactjs",  // ❌ 오류 발생
};

여기서 book2 변수는 Book 타입으로 선언되었지만, **초과 프로퍼티인 **skill****이 포함되어 있습니다. Book 타입에는 **name**과 **price**만이 정의되어 있기 때문에, **skill**은 타입스크립트의 초과 프로퍼티 검사에 의해 오류가 발생합니다.

예외 상황:

객체 리터럴이 아닌 경우에는 초과 프로퍼티 검사가 적용되지 않을 수 있습니다. 예를 들어, 다음과 같은 경우는 문제가 발생하지 않습니다.

let programmingBook: ProgrammingBook = {
  name: "한 입 크기로 잘라먹는 리액트",
  price: 33000,
  skill: "reactjs",
};

let book: Book = programmingBook;  // ✅ OK, 초과 프로퍼티 검사 없음

programmingBook 객체는 Book 타입 변수에 할당되지만, 이 경우 객체 리터럴이 직접적으로 사용된 것이 아니기 때문에 초과 프로퍼티 검사가 이루어지지 않고 정상적으로 동작합니다.

요약

  • 객체 타입의 호환성: 더 많은 프로퍼티를 가진 객체 타입(Dog, ProgrammingBook)은 적은 프로퍼티를 가진 객체 타입(Animal, Book)에 할당할 수 있습니다. 그러나 그 반대는 불가능합니다.
  • 초과 프로퍼티 검사: 객체 리터럴에 추가된 불필요한 프로퍼티가 있으면 타입스크립트가 오류를 발생시킵니다. 하지만 객체 리터럴이 아닌 경우, 초과 프로퍼티 검사는 적용되지 않습니다.

타입스크립트의 이러한 구조적 타입 시스템과 초과 프로퍼티 검사는 코드를 안전하게 유지하고, 실수로 잘못된 데이터 구조를 사용하는 것을 방지하는 데 매우 효과적입니다.


대수 타입(Algebraic Type)

대수 타입은 여러 개의 타입을 조합하여 새로운 타입을 정의하는 방식입니다. 주로 **합집합 타입(Union Type)**과 **교집합 타입(Intersection Type)**으로 나뉘며, 이는 각각 여러 타입 중 하나를 선택하는 방식과 여러 타입을 동시에 만족하는 방식으로 이해할 수 있습니다.

이를 통해 타입스크립트에서는 더욱 유연하고 강력한 타입 시스템을 구현할 수 있습니다. 대수 타입을 이해하기 위해 합집합 타입과 교집합 타입을 차례대로 살펴보겠습니다.

1. 합집합(Union) 타입

합집합 타입(Union Type)은 여러 타입 중 하나를 가질 수 있는 타입입니다. 이를 정의할 때는 | 연산자를 사용하여 나열된 타입들 중 하나의 타입을 허용하는 변수를 만들 수 있습니다.

예시 1: 기본적인 합집합 타입

let a: string | number;

a = 1;       // number 타입
a = "hello"; // string 타입

여기서 변수 **a**는 string 또는 number 타입을 가질 수 있습니다. 즉, **a**는 두 타입 중 하나로 선언될 수 있으며, 이를 집합으로 표현하면 **a**는 두 집합의 합집합에 속하는 값을 가질 수 있습니다.

예시 2: 복합적인 합집합 타입

let arr: (number | string | boolean)[] = [1, "hello", true];

여기서는 numberstringboolean 타입을 허용하는 배열을 정의했습니다. 배열 안에 다양한 타입의 값을 저장할 수 있다는 점이 특징입니다.

예시 3: 객체 타입에서의 합집합

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

type Person = {
  name: string;
  language: string;
};

type Union1 = Dog | Person;

let union1: Union1 = {
  name: "돌돌이",
  color: "brown"
};

let union2: Union1 = {
  name: "홍길동",
  language: "Korean"
};

let union3: Union1 = {
  name: "John",
  color: "white",
  language: "English"
};
  • **Union1**은 Dog 타입 또는 Person 타입을 가질 수 있습니다.
  • 교집합이 존재하는 프로퍼티는 **name**으로, 이는 두 타입 모두에 존재합니다.
  • **union1**과 **union2**는 각각 **Dog**와 Person 타입에 맞게 정의되었습니다.
  • **union3**은 두 타입의 프로퍼티 모두를 포함하여도 문제없습니다.

하지만, 다음처럼 모든 필수 프로퍼티가 정의되지 않으면 타입 오류가 발생합니다.

let union4: Union1 = {
  name: "기린"
}; // ❌ 오류 발생 (color 또는 language 누락)

2. 교집합(Intersection) 타입

교집합 타입(Intersection Type)은 두 타입을 모두 만족하는 타입을 정의합니다. & 연산자를 사용하여 교집합 타입을 정의할 수 있으며, 이는 두 타입의 프로퍼티를 모두 가져야 합니다.

예시 1: 기본적인 교집합 타입

let variable: number & string;
// ❌ `number`와 `string`은 교집합이 없으므로 `never` 타입으로 추론됨

기본 타입인 **number**와 string 사이에는 교집합이 없으므로, 이 변수는 실제로 never 타입으로 추론됩니다. 즉, 서로 다른 기본 타입 간에는 교집합이 성립되지 않습니다.

예시 2: 객체 타입에서의 교집합

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

type Person = {
  name: string;
  language: string;
};

type Intersection = Dog & Person;

let intersection1: Intersection = {
  name: "돌돌이",
  color: "brown",
  language: "Korean"
};
  • Intersection 타입은 **Dog**와 **Person**의 모든 프로퍼티를 포함하는 객체 타입입니다.
  • 교집합을 사용하면 두 타입의 모든 필수 프로퍼티를 포함해야 하므로, **intersection1**은 namecolor, **language**를 모두 정의해야 합니다.
  • 모든 프로퍼티를 만족하는 객체를 만들어야 하므로, 보다 엄격한 타입 검사가 이루어집니다.

3. 대수 타입 통합 예시

다음은 합집합 타입과 교집합 타입을 함께 사용하는 예시입니다:

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

type Person = {
  name: string;
  language: string;
};

type UnionType = Dog | Person;
type IntersectionType = Dog & Person;

let unionExample1: UnionType = {
  name: "돌돌이",
  color: "brown"
};

let unionExample2: UnionType = {
  name: "홍길동",
  language: "Korean"
};

let intersectionExample: IntersectionType = {
  name: "돌돌이",
  color: "brown",
  language: "Korean"
};

// unionExample1은 Dog 타입으로, unionExample2는 Person 타입으로 처리됩니다.
// intersectionExample은 Dog와 Person의 모든 속성을 가져야 하므로 모든 필수 필드를 포함해야 합니다.

4. 정리

  • 합집합(Union) 타입: 여러 타입 중 하나의 타입만 만족하면 되는 타입. 더 유연한 타입 검사가 가능하지만, 각 타입에 맞는 타입 검사와 타입 가드를 잘 활용해야 합니다.
  • 교집합(Intersection) 타입: 여러 타입을 모두 만족해야 하는 타입. 두 타입의 모든 프로퍼티를 포함해야 하므로 더 엄격한 타입 검사가 이루어집니다.
  • 합집합과 교집합의 차이: 합집합은 선택 가능한 타입을 넓히는 것이고, 교집합은 모든 조건을 만족하도록 강제하는 것이 차이점입니다.

대수 타입을 적절히 활용하면 타입스크립트에서 더 강력하고 유연한 타입 시스템을 구성할 수 있습니다.


타입 추론(Type Inference)

타입 추론이란 타입스크립트가 명시적으로 타입을 선언하지 않은 변수나 함수의 타입을 자동으로 추론하는 기능을 말합니다. 이를 통해 프로그래머는 모든 변수나 함수의 타입을 직접 정의하지 않아도 타입스크립트가 자동으로 타입을 유추하여 코드의 안정성을 보장해줍니다.

1. 타입 추론의 기본 예시

타입스크립트는 다음과 같이 변수 선언 시 초기값을 기준으로 타입을 추론합니다.

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

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

이처럼 변수 선언 시 초기값을 제공하면, 타입스크립트가 해당 값의 타입을 기준으로 자동으로 타입을 지정합니다. 하지만 모든 상황에서 타입을 자동으로 추론하는 것은 아닙니다. 예를 들어, 함수 매개변수의 타입은 명시적으로 정의하지 않으면 암시적으로 **any**로 추론됩니다.

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

2.1 변수 선언

초기값이 주어진 변수는 타입스크립트에 의해 자동으로 타입이 추론됩니다. 복잡한 객체도 타입을 문제없이 추론할 수 있습니다.

let c = {
  id: 1,
  name: "김수현",
  profile: {
    nickname: "winterlood",
  },
  urls: ["<https://winterlood.com>"],
};
// 객체 타입을 자동으로 추론

이 경우, 타입스크립트는 c 변수를 { id: number, name: string, profile: { nickname: string }, urls: string[] } 타입으로 추론합니다.

2.2 구조 분해 할당

구조 분해 할당에서도 타입은 자동으로 추론됩니다.

let { id, name, profile } = c;

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

위 코드에서 idname, **profile**은 각각 c 객체의 속성 타입에 맞게 추론됩니다. 배열도 각각의 타입을 추론하여 **one**은 number, **two**는 string, **three**는 **boolean**으로 추론됩니다.

2.3 함수의 반환값

함수의 반환값은 return 문을 기준으로 타입이 추론됩니다.

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

이 함수는 string 타입을 반환하므로, 함수의 반환 타입은 자동으로 **string**으로 추론됩니다.

2.4 기본값이 설정된 매개변수

기본값이 설정된 매개변수는 기본값을 기준으로 타입을 추론합니다.

function func(message = "hello") {
  return message;
}
// message 매개변수는 string 타입으로 추론

여기서는 message 매개변수에 기본값 **"hello"**가 할당되어 있으므로, 타입스크립트는 이를 string 타입으로 추론합니다.

3. 주의해야 할 상황들

3.1 암시적인 any 타입

변수를 선언할 때 초기값을 생략하면 해당 변수는 any 타입으로 추론됩니다. 특히 함수 매개변수도 명시적인 타입을 주지 않으면 any 타입으로 추론됩니다.

let d;
// 암시적인 any 타입

d = 10;
d.toFixed(); // number 타입 메서드 사용 가능

d = "hello";
d.toUpperCase(); // string 타입 메서드 사용 가능
d.toFixed(); // ❌ 오류: d는 현재 string 타입

위 코드에서는 **d**가 처음에는 **any**로 추론되지만, 값이 할당됨에 따라 타입이 변하는 현상을 볼 수 있습니다. 이런 현상을 any 타입의 진화라고도 합니다.

3.2 const 상수의 타입 추론

**const**로 선언된 상수는 초기값을 변경할 수 없기 때문에, 타입스크립트는 이를 리터럴 타입으로 추론합니다.

const num = 10;
// 10 리터럴 타입으로 추론 (number가 아닌 값 10 자체)

const str = "hello";
// "hello" 리터럴 타입으로 추론 (string이 아닌 "hello" 자체)

상수는 변경되지 않으므로 가장 좁은 범위의 리터럴 타입으로 추론됩니다.

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

타입스크립트는 배열과 같은 여러 요소가 섞인 상황에서 모든 요소의 공통된 타입을 기준으로 타입을 추론합니다.

let arr = [1, "string"];
// (string | number)[] 타입으로 추론

이 경우, 배열 **arr**에는 **number**와 string 타입이 모두 포함되어 있으므로 최적의 공통 타입인 string | number 타입을 추론합니다.

4. 정리

  • 타입 추론은 타입스크립트가 자동으로 변수나 함수의 타입을 결정하는 기능입니다.
  • 변수 선언, 구조 분해 할당, 함수 반환값, 기본값이 있는 매개변수에서 타입을 잘 추론합니다.
  • 초기값이 없는 변수나 함수 매개변수는 암시적으로 any 타입으로 추론됩니다. strict 모드에서는 이 상황이 오류로 간주됩니다.
  • const 상수는 리터럴 타입으로 추론되어, 값 자체의 타입이 유지됩니다.
  • 최적 공통 타입은 배열 등 여러 타입이 섞인 상황에서 공통 타입으로 자동 추론됩니다.

타입 추론을 통해 더 명확하고 안전한 코드를 작성할 수 있지만, 타입 추론이 불명확한 경우에는 직접 타입을 명시하는 것이 좋습니다.


타입 단언(Type Assertion)

타입스크립트에서는 때때로 컴파일러가 타입을 제대로 추론하지 못하거나, 개발자가 명시적으로 타입을 단언하고 싶은 경우가 있습니다. 이럴 때 타입 단언을 사용하여 값을 특정 타입으로 간주하도록 타입스크립트에게 지시할 수 있습니다.

1. 기본적인 타입 단언

타입 단언은 **값 as 타입**의 형식으로 사용되며, 특정 값을 개발자가 원하는 타입으로 강제할 수 있습니다.

예시: 빈 객체 초기화

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

let person = {} as Person;
person.name = "";
person.age = 23;
  • person 변수는 Person 타입으로 정의되었지만, 빈 객체를 초기화하고 싶을 때 타입스크립트는 이를 허용하지 않습니다. 빈 객체는 Person 타입이 아니기 때문입니다.
  • 이럴 때 **{} as Person**을 사용하여 빈 객체를 강제로 Person 타입으로 단언할 수 있습니다. 이후 **name**과 age 프로퍼티를 추가할 수 있습니다.

2. 초과 프로퍼티 검사 무시

타입 단언을 사용하면 초과 프로퍼티 검사를 무시할 수 있습니다.

예시: 초과 프로퍼티 무시

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

let dog: Dog = {
  name: "돌돌이",
  color: "brown",
  breed: "진도",  // 초과 프로퍼티
} as Dog;
  • Dog 타입에는 **name**과 color 프로퍼티만 존재하지만, **breed**라는 초과 프로퍼티를 추가했습니다.
  • 타입스크립트는 초과 프로퍼티가 있으면 오류를 발생시키지만, **as Dog**로 타입 단언을 통해 이를 무시할 수 있습니다.

3. 타입 단언의 조건

타입 단언에는 다음 두 가지 조건이 있습니다. **값 as 타입**에서:

  1. A(값의 타입)가 B(단언할 타입)의 슈퍼타입이거나
  2. A(값의 타입)가 B(단언할 타입)의 서브타입이어야 합니다.

예시: 슈퍼타입과 서브타입

let num1 = 10 as never;   // ✅
let num2 = 10 as unknown; // ✅
let num3 = 10 as string;  // ❌ 오류 발생
  • **num1**은 number 타입을 never 타입으로 단언할 수 있습니다. **never**는 모든 타입의 서브타입이므로 단언이 가능합니다.
  • **num2**는 number 타입을 unknown 타입으로 단언할 수 있습니다. **unknown**은 모든 타입의 슈퍼타입이므로 역시 단언이 가능합니다.
  • 그러나 **num3**은 number 타입을 string 타입으로 단언하려 했습니다. 하지만 **number**와 **string**은 서로 슈퍼-서브 관계가 아니므로 단언이 불가능하고 오류가 발생합니다.

4. 다중 단언

타입 단언은 다중으로 사용할 수도 있습니다. 예를 들어, 앞서 불가능했던 단언도 중간에 **unknown**을 거쳐서 가능하게 만들 수 있습니다.

let num3 = 10 as unknown as string;
  • 여기서는 number 타입을 unknown 타입으로 단언한 다음, 다시 unknown 타입을 string 타입으로 단언합니다.
  • unknown 타입은 모든 타입의 슈퍼타입이므로 다중 단언을 통해 이러한 단언이 가능해집니다.

하지만 다중 단언은 매우 위험할 수 있습니다. 실제로 값의 타입을 변경하지 않으면서 단순히 컴파일러를 속이는 것이기 때문에 잘못된 타입 단언이 발생할 가능성이 높아집니다. 따라서 최대한 사용을 자제하는 것이 좋습니다.

5. const 단언

타입 단언에서는 const 단언도 존재합니다. 이를 사용하면 해당 값을 리터럴 타입으로 단언하거나, 객체의 모든 프로퍼티를 ****readonly****로 설정할 수 있습니다.

예시: const 단언

let num4 = 10 as const;
// num4는 10 리터럴 타입으로 추론됨

let cat = {
  name: "야옹이",
  color: "yellow",
} as const;
// cat 객체의 모든 프로퍼티는 readonly로 설정됨
  • **num4**는 단순한 number 타입이 아니라 10 리터럴 타입으로 추론됩니다. 즉, **num4**는 값이 10인 상수로 간주됩니다.
  • cat 객체의 프로퍼티들은 모두 readonly로 설정되므로, 이후에 프로퍼티 값을 변경할 수 없습니다.

6. Non-null 단언 연산자

타입스크립트에서는 Non-null 단언 연산자(!)를 사용하여 값이 **null**이나 **undefined**가 아님을 단언할 수 있습니다.

예시: Non-null 단언 연산자

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

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

const len: number = post.author!.length;  // Non-null 단언
  • **post.author**는 string 타입일 수도 있고 **undefined**일 수도 있습니다.
  • 하지만 개발자는 post.author가 null이나 undefined가 아니라고 확신할 때 ! 연산자를 사용하여 타입스크립트에게 이를 알릴 수 있습니다.
  • 이 경우, 타입스크립트는 **post.author**가 **null**이나 **undefined**가 아님을 단언하고, **length**를 안전하게 호출할 수 있습니다.

7. 정리

  • 타입 단언은 개발자가 값의 타입을 명시적으로 선언하는 방식입니다. 값 as 타입 구문을 통해 특정 값을 원하는 타입으로 간주하도록 할 수 있습니다.
  • 초과 프로퍼티 검사나 빈 객체 초기화처럼 타입스크립트의 엄격한 타입 검사 규칙을 우회하는 데에 사용될 수 있습니다.
  • 타입 단언의 조건: 값의 타입이 단언할 타입의 슈퍼타입이거나 서브타입이어야 합니다.
  • 다중 단언을 사용하면 불가능한 단언을 가능하게 만들 수 있지만, 이는 위험할 수 있어 최대한 피하는 것이 좋습니다.
  • const 단언을 사용하면 리터럴 타입으로 단언하거나 객체의 모든 프로퍼티를 **readonly**로 설정할 수 있습니다.
  • Non-null 단언 연산자(!)를 사용하여 값이 **null**이나 **undefined**가 아님을 단언할 수 있습니다.

타입 단언은 타입스크립트의 강력한 타입 시스템을 잠시 우회하는 기능이므로, 신중하게 사용해야 하며, 타입 안전성을 해칠 수 있으므로 필요할 때만 사용하도록 하는 것이 좋습니다.


타입 좁히기 (Type Narrowing)

타입 좁히기란 타입스크립트에서 유니온 타입처럼 여러 타입이 허용된 변수를 특정 조건을 통해 더 구체적인 타입으로 좁히는 과정을 말합니다. 이를 통해 변수가 특정 타입임을 보장할 수 있으며, 타입스크립트는 이 과정에서 변수의 타입을 안전하게 추론할 수 있습니다. 타입 좁히기는 조건문과 함께 사용되어, 특정 조건 안에서 변수의 타입을 좁히는 방식으로 동작합니다. 이를 **타입 가드(Type Guard)**라고도 부릅니다.

1. 기본적인 타입 좁히기

다음과 같은 함수가 있다고 가정해보겠습니다:

function func(value: number | string) {
  if (typeof value === "number") {
    console.log(value.toFixed());
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());
  }
}
  • 여기서 ****value***는 number 또는 string 타입을 가질 수 있습니다.
  • typeof 연산자를 사용하여 **value**가 number 타입일 때와 string 타입일 때를 구분하여 타입을 좁힐 수 있습니다.
  • 타입 가드를 통해 조건문 내부에서는 타입이 좁혀지기 때문에 **value.toFixed()**와 value.toUpperCase() 메서드를 안전하게 호출할 수 있습니다.

2. 타입 가드 (Type Guard)

타입 가드는 조건문과 함께 특정 타입을 보장하는 방법입니다. typeofinstanceofin 연산자를 통해 타입을 좁힐 수 있으며, 이를 사용해 변수의 타입을 안전하게 사용할 수 있는 환경을 만들어줍니다.

3. 타입 가드: typeof

typeof 연산자를 사용하면 기본적인 원시 타입을 체크할 수 있습니다. numberstring, **boolean**과 같은 원시 타입은 typeof 연산자를 통해 확인할 수 있습니다.

function func(value: number | string) {
  if (typeof value === "number") {
    console.log(value.toFixed());  // number로 타입이 좁혀짐
  } else if (typeof value === "string") {
    console.log(value.toUpperCase());  // string으로 타입이 좁혀짐
  }
}

4. 타입 가드: instanceof

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());  // Date로 타입이 좁혀짐
  }
}
  • 여기서는 Date 객체가 전달되었을 때, **value**가 Date 타입임을 보장하고 getTime() 메서드를 호출할 수 있습니다.
  • 주의: **instanceof**는 클래스에만 적용할 수 있기 때문에, 인터페이스나 객체 리터럴에는 사용할 수 없습니다.

5. 타입 가드: in 연산자

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}살 입니다`);
  }
}
  • Person 타입의 객체가 전달되었을 때, in 연산자를 사용하여 age라는 프로퍼티가 존재하는지 확인할 수 있습니다.
  • 타입스크립트는 **age**라는 프로퍼티가 존재하면 **value**가 Person 타입임을 추론하고, 그에 맞는 프로퍼티를 안전하게 사용할 수 있게 됩니다.

6. 정리

타입 좁히기는 유니온 타입과 같은 여러 타입을 허용하는 변수를 특정 조건을 통해 더 구체적인 타입으로 좁히는 과정입니다. 타입 좁히기를 사용하면 타입스크립트의 타입 시스템을 더욱 안전하게 사용할 수 있으며, 타입 가드를 통해 변수를 다양한 상황에서 올바르게 처리할 수 있습니다.

  • typeof: 기본적인 원시 타입의 타입 좁히기에 사용됩니다.
  • instanceof: 내장 클래스 또는 사용자 정의 클래스의 객체 타입을 좁히는 데 사용됩니다.
  • in: 특정 객체의 프로퍼티를 검사하여, 객체 타입을 좁히는 데 사용됩니다.

타입스크립트의 타입 가드와 타입 좁히기 기능을 잘 활용하면, 코드가 안전하고 예측 가능하게 동작하며, 유연한 타입 처리가 가능합니다.


서로소 유니온 타입(Discriminated Union Type)

서로소 유니온 타입은 교집합이 없는 타입들을 모아서 만든 유니온 타입을 의미합니다. 즉, 서로 간에 공통된 속성을 가지지 않는 타입들로 이루어진 유니온 타입입니다. 이와 같은 타입을 사용하면 타입스크립트에서 안전하고 명확한 타입 가드를 구현할 수 있습니다.

서로소 유니온 타입의 핵심은 태그된 유니온(Tagged Union) 또는 **구별된 유니온(Discriminated Union)**을 사용하는 방법입니다. 여기서 태그 프로퍼티는 각 타입을 구분하는 리터럴 타입의 프로퍼티를 뜻합니다. 이를 통해 조건문 없이도 타입을 명확하게 구분할 수 있습니다.

예시: 회원 관리 프로그램

다음은 회원 관리 프로그램에서 회원의 역할을 AdminMemberGuest로 나누어 서로 다른 정보를 처리하는 예입니다.

1. 기본적인 서로소 유니온 타입 예시

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}번 방문하셨습니다.`);
  }
}

문제점: 직관성 부족

위 코드에서 in 연산자를 사용해 프로퍼티에 따라 타입을 좁히는 방식을 사용했습니다. 그러나 이렇게 작성하면 코드만 보고 어떤 타입이 선택되는지 직관적으로 알기 어려워집니다.

해결 방법: 태그 프로퍼티를 사용한 타입 구분

이 문제를 해결하기 위해 각 타입에 태그 프로퍼티를 추가하여 타입을 구분할 수 있습니다. 리터럴 타입으로 구별되는 tag 프로퍼티를 추가해 더 직관적이고 명확하게 타입을 구분할 수 있습니다.

2. 태그 프로퍼티를 추가한 서로소 유니온 타입

type Admin = {
  tag: "ADMIN";  // "ADMIN" 리터럴 타입
  name: string;
  kickCount: number;
};

type Member = {
  tag: "MEMBER";  // "MEMBER" 리터럴 타입
  name: string;
  point: number;
};

type Guest = {
  tag: "GUEST";  // "GUEST" 리터럴 타입
  name: string;
  visitCount: number;
};

type User = Admin | Member | Guest;

이제 각 타입에 리터럴 타입의 tag 프로퍼티를 추가함으로써 타입 가드를 더 명확하게 작성할 수 있습니다.

3. 태그 프로퍼티를 사용한 타입 가드

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}번 방문하셨습니다.`);
  }
}
  • 이제 user.tag 값에 따라 유저의 타입이 명확하게 구분됩니다. tag 프로퍼티를 기준으로 타입이 좁혀지기 때문에 조건식이 더욱 직관적이고 코드를 읽기 쉬워졌습니다.

4. switch문을 사용한 더 직관적인 타입 가드

**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;
    }
  }
}
  • **switch**문을 사용하면 조건문 없이 각 타입을 리터럴 값에 따라 분리할 수 있어 가독성이 높아집니다.

5. 정리

  • 서로소 유니온 타입은 교집합이 없는 타입들을 모아 만든 유니온 타입입니다. 각 타입에 공통 프로퍼티가 없기 때문에 구분이 명확하며, 이 방식으로 타입 안전성을 확보할 수 있습니다.
  • *태그 프로퍼티(Discriminated Union)**는 각 타입에 구별되는 리터럴 타입을 추가하여 타입을 더 명확하고 직관적으로 좁히는 방식입니다.
  • in 연산자나 태그 프로퍼티를 사용하여 타입 가드를 구현하면, 타입스크립트의 타입 안전성을 유지하면서도 가독성이 높은 코드를 작성할 수 있습니다.
  • switch문을 사용하면 태그 프로퍼티 기반의 타입 가드를 더 직관적이고 명확하게 구현할 수 있습니다.

이러한 방식으로 서로소 유니온 타입을 사용하면, 복잡한 유니온 타입에서도 안전하고 예측 가능한 타입 처리가 가능합니다.

'TypeScript' 카테고리의 다른 글

TypeScript 인터페이스  (0) 2024.10.29
TypeScript 정리3 (함수와 타입)  (0) 2024.10.25
TypeScript 정리1  (1) 2024.10.25
TypeScript 기본  (3) 2024.10.25
TypeScript 개론  (0) 2024.10.25
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유