FrontEnd/TypeScript

[TypeScript] 가변성(슈퍼타입/서브타입 파악하기) & 할당성 & 타입 넓히기

개발자 김비숑 2023. 6. 3. 18:18
반응형

1. 타입 간의 관계 

1. 1 서브 타입과 슈퍼 타입 

  • A가 필요한 곳에 B를 사용할 수 있다면 A는 슈퍼 타입, B는 서브 타입이다. 
  • 예를 들어, 객체를 사용해야 하는 곳에 배열도 사용할 수 있다. 따라서 이 경우 객체는 슈퍼 타입, 배열은 서브 타입이다. 
  • 모든 것은 any의 서브타입이고, never는 모든 것의 서브타입이다. 즉, 모든 타입을 any에 할당할 수 있고, never는 모든 타입에 할당할 수 있다. 

1.2 가변성

  • 단순 타입은 이 관계를 파악하기 쉽다. 예) number 는 number | string 유니온의 서브 타입이다. 
  • 하지만 객체나 함수와 같은 복합 타입의 경우 이 관계를 파악하기가 복잡할 수 있다. 복합 타입의 서브타입 규칙은 언어마다 다르다.
type ExistingUsesr = {
  id: number
  name: string
}

type NewUser = {
  name: string
}

function deleteUser(user: {id?:number, name: string}) {
  delete user.id
}

let existingUser:ExistingUsesr = {
  id: 12345,
  name: 'Ima User'
}

/**
 * {id: number | undefined, name: string }은 슈퍼타입
 * {id: number,name: string }은 서브타입. (각각의 프로퍼티를 비교)
 * 
 * 서브타입 -> 슈퍼타입에 할당 가능 (공변성)
 */
deleteUser(existingUser)
const id = existingUser.id // 삭제되었지만 number 타입으로 추론하는 문제

형태와 배열 가변성

  • 가변성 : 타입과 서브타입의 관계 파악하기 
    1. 불변 
      • 정확히 T만 할당 가능
    2. 공변(대부분의 경우)
      • A가 B의 서브타입이면, T<A>는 T<B>의 서브타입이다. 
      • 예를 들어, string은 string | number의 서브타입인 경우를 보자. Array<string>은 Array<string | number>의 서브타입이고, { a: string, b: number }도 { a: string | number, b: number }의 서브타입이 된다.  
    3. 반변
      • A가 B의 서브타입이면, T<B>는 T<A>의 서브타입이다.
      • 예를 들어, logNum 함수타입이 logNum = (params :number) => void이고, log 함수의 타입이 log = (params: number | string) => void라고 하자. 이 경우 매개변수 기준으로 logNum 함수의 매개변수가 서브 타입이므로 함수 logNum이 슈퍼타입이 된다. 
    4. 양변
      • A가 B의 서브타입이면, 서브타입 -> 슈퍼타입, 슈퍼타입 -> 서브타입 모두 할당 가능.
  • 타입스크립트의 할당 규칙 : 타입스크립트 설계자들은 쉬운 사용과 안전성 사이의 균형을 중시
    • 모든 복합타입의 멤버(객체, 클래스, 배열, 함수, 반환 타입)은 공변
    • 함수 매개변수 타입만 반변
    • 결론적으로 대부분의 경우 서브타입을 슈퍼타입에 할당할 수 있다.
    • 한 가지 유의해야 할 부분은 함수 타입 간 슈퍼/서브 관계를 파악할 때이다.
      • 반환타입과 this타입이 슈퍼타입인 타입이 슈퍼타입이 된다. 
      • 매개변수가 다른 경우, 매개변수가 슈퍼타입인 타입이 서브타입이 된다. 
// 함수의 슈퍼타입과 서브타입 관계 이해하기 
// 슈퍼타입과 서브타입 만들기
class Animal {}
class Bird extends Animal {
  chirp() {}
}
class Crow extends Bird {
  caw() {}
}

// 1. 반환 타입(공변)
function clone(f: (b: Bird) => Bird): void {}


function birdToBird(b: Bird): Bird { return new Bird }
// 반환타입이 슈퍼타입인 경우 -> 함수타입도 슈퍼타입으로 판단 
function birdToAnimal(b: Bird) : Animal { return new Animal }
// 반환타입이 서브타입인 경우 -> 함수 타입도 서브타입으로 판단
function birdToCrow(b: Bird) : Crow { return new Crow }

clone(birdToBird)
clone(birdToAnimal) // Animal은 Bird에 필요한 chirp 함수가 없음. 슈퍼타입 -> 서브타입에 할당 불가. 
clone(birdToCrow) // 서브타입 -> 슈퍼타입 할당 가능 


// 2. 매개변수 타입(반변)
// 매개변수는 더 큰 범위를 받아야 항상 필수인 타입을 가지고 있기 때문. 
// 매개변수 타입이 슈퍼타입인 경우 -> 함수타입은 서브타입
function animalToBird(b: Animal) : Bird { return new Bird }
// 매개변수 타입이 서브타입인 경우 -> 함수타입은 슈퍼타입
function crowToBird(b: Crow) : Bird { return new Crow }

clone(animalToBird) // 서브타입 -> 슈퍼타입 할당 가능
clone(crowToBird) // Bird에는 Crow에 필요한 caw함수가 없음.

 

1.3 할당성

  • 슈퍼타입과 서브타입의 관계를 파악하면 타입 할당 가능 여부도 알 수 있다. 
  • A를 B에 할당 가능한지(할당성)를 결정할 때 타입스크립트는 몇 가지 규칙을 확인한다. (열거형 이외 경우)
    • 서브타입은 슈퍼타입에 할당할 수 있다. 
    • A는 any인 경우 -> 자바스크립트와 코드 파악할 때 유용)
  • 열거형 타입(enum, const enum)의 경우 다음 조건 중 하나를 만족해야 A타입을 열거형 B에 할당할 수 있다.
    • A는 열거형 B의 멤버이다.
    • B는 number 타입의 멤버를 최소 한 개 이상 가지고 있으며 A는 number이다. 
    • 열거형을 사용하지 말자! 

 

1.4 타입 넓히기 

  • 타입 넓히기는 타입스크립트가 타입을 일반적으로 추론하는 추론 방식을 말한다. 
  • let이나 var로 변수를 선언하면 그 변수의 타입이 타입 리터럴에서 리터럴 값이 속한 기본 타입으로 넓어진다.
let a1 = 'x'  // string 
const a2 = 'x' // "x"

let b1 = true // boolean
const b2 = true // true

//타입을 명시하면 타입이 넓혀지지 않게 할 수 있다.
let a3:'x' = 'x' // "x"
let b3:true = true // true

// 자동 확장
const a4 = 'x' // 'x'
let x = a4 // string
  • null이나 undefined로 초기화 된 변수는 any 타입으로 넓혀진다
let a = null // any
a = 3 // any
a = 'b' // any

// 선언범위를 벗어나는 경우 좁은 타입 할당
function fn() {
  let a = null // any
  a = 'abc' // any
}

fn() // a는 string
  • const를 사용하면 타입이 넓혀지지 않도록 할 수 있다. 
    • const를 사용하면 타입 넓히기가 중지되고 멤버들까지 자동으로 readonly가 된다.

 

초과 프로퍼티 확인

  • 한 객체타입을 다른 객체 타입에 할당할 수 있는지를 확인할 때도 타입 넓히기를 이용한다. 
  • 객체타입과 프로퍼티는 공변 관계이지만, 초과 프로퍼티 확인 기능 덕분에 이를 검출할 수 있다. 
  • 타입스크립트는 안전성을 확보하기 위해 신선한 객체 리터럴 타입 T 다른 타입 U에 할당하려는 경우 T가 U에 존재하지 않는 프로퍼티를 추가적으로 가지고 있다면 에러를 발생한다.
  • 신선한 객체 리터럴 타입이란 타입스크립트가 객체 리터럴로부터 추론한 타입을 뜻한다. 객체 리터럴이 타입 어서션을 사용하거나 변수에 할당하면 일반 객체 타입으로 넓혀지면서 더 이상 신선하지 않게 된다. 
type Options = {
  baseURL: string,
  cacheSize?: number
  tier?:'prod' | 'dev'
}

class API {
  constructor(private options: Options) { }
}

// 1. Good ! 
new API({
  baseURL: 'https://api.mysite.com',
  tier: 'prod'
})

// 2. 신선한 객체 리터럴 타입 -> 초과 프로퍼티 확인ㅍ-> 에러 발생 O
new API({
  baseURL: 'https://api.mysite.com',
  tier: 'prod'
  someNewValue: 'value' // someNewValue는 Options에 존재하지 않아 에러 발생 
})

// 3. 타입 어서션 -> 에러 발생 X
new API({
  baseURL: 'https://api.mysite.com',
  wrongTier: 'prod'
} as Options)

// 변수에 할당
let wrongOptions = {
  baseURL: 'https://api.mysite.com',
  wrongTier: 'prod'
}

new API(wrongOptions)
  • 이런 규칙들은 모두 외울 필요 없이 타입스크립트 내부 규칙이 이러한 버그를 잡아준다는 사실만 잘 이해하면 된다. 
반응형