TIR: Typescript | 6. 고급 타입

Lumpen·2023년 2월 21일
0

Typescript

목록 보기
15/17

타입스크립트는 세계 최고 수준의 타입 시스템을 자랑한다

타입 간의 관계

서브타입과 슈퍼타입

서브타입

두 개의 타입 A, B 가 있고 B가 A의 서브타입이면 A 가 필요한 곳에는 어디든
B 를 안전하게 사용할 수 있다

서브타입의 예

  • 배열은 객체의 서브타입이다
  • 튜플은 배열의 서브타입이다
  • 모든 것은 any 의 서브타입이다
  • never 는 모든 것의 서브타입이다

위 예시에 서브타입의 정의를 적용하면 다음과 같이 설명할 수 있다 (다형성)

  • 객체를 사용해야 하는 곳에는 배열도 사용할 수 있다
  • 배열을 사용해야 하는 곳에는 튜플도 사용할 수 있다
  • any 를 사용해야 하는 곳에는 어떤 것도 사용 가능하다
  • 어디에나 never 를 사용할 수 있다

슈퍼타입

두 개의 타입 A, B 가 있고 B가 A 의 슈퍼타입이면 B 가 필요한 곳에는 어디든 
A를 안전하게 사용할 수 있다
B: 슈퍼타입
A: 서브타입
  • 객체는 배열의 슈퍼타입이다
  • 배열은 튜플의 슈퍼타입이다
  • any 는 모든 것의 슈퍼타입이다
  • never 는 어떤 것의 슈퍼타입이 아니다

가변성

보통 A 라는 타입이 B 라는 다른 타입의 서브타입인지 아닌지 쉽게 판단할 수 있다
단순 타입은 흐름도로 확인하거나 자체적으로 쉽게 추론 가능하다
number 는 number | string 의 서브타입이다

매개변수화된 (제네릭) 타입 등 복합 타입에서는 조금 복잡해진다

  • Array<A>는 어떤 상황에 Array<B>의 서브타입이 되는가
  • 형태 A 는 어떤 상황에서 다른 형태 B 의 서브타입이 되는가
  • 함수 (a: A) => B 는 어떤 상황에서 다른 함수 (c: C) => D 의 서브타입이 되는가

다른 타입을 포함하는 Array<A> 같은 타입의 서브타입 규칙을 추론하기는 어렵고
프로그래밍 언어마다 조금씩 차이가 있다

형태와 배열 가변성

타입스크립트의 타입 규칙은 안전성을 추구하도록 설계되었다기보다
실수를 캐치하는 것과 쉬운 사용 두 가지를 목표로 한 것
안전성이 보장되지 않는 특정한 상황은 실용적인 면에서 타당성이 있다
프로퍼티 삭제 등의 파괴적 갱신은 실무에서 비교적 드물게 일어나므로
타입스크립트는 이를 적극 제지하지 않고
슈퍼타입이 필요한 곳에 객체를 할당할 수 있도록 허용한다

type ExistingUser = {
	id: number
  	name: string
}

type NewUser = {
	name: string
}

위와 같은 유저를 삭제한다고 할 때

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

let existingUser: ExistingUser = {
	id: 123,
  	name: 'string'
}

deleteUser(existingUSer)

deleteUser 의 user 매개변수 id 프로퍼티 타입이 (number | undefined) 의
서브타입이 되므로 전체 객체 existingUser 도 에러를 발생시키지 않는다
ExistringUser 를 deleteUser 로 전달, 삭제 후에도
타입스크립트는 user 의 id 가 삭제된 사실을 모른다
따라서 id 삭제 후에도 existingUser 의 id 를 타입스크립트는 number 타입으로
생각한다

반대 방향 ( 서브타입이 필요한 곳에 할당 ) 하는 경우는 다음과 같다

type LegacyUser = {
	id?: number | string
  	name: string
}

let legacyUser: LegacyUser = {
	id: '123',
  	name: 'name'
}

deleteUser(lagacyUser) // 에러 TS2346

기대하는 타입의 슈퍼타입의 프로퍼티를 포함하는 형태를 전달하면
타입스크립트는 에러를 발생시킨다

슈퍼타입의 id 는 number | string | undefined 인 반면
deleteUser 는 id 가 number | undefined 인 경우에만 처리할 수 있다

요구되는 프로퍼티 타입 각각에 대해 기대되는 타입이 요구되는 타입과 같거나 포함되어야 한다
즉 정의된 객체의 프로퍼티 타입이거나 해당 타입의 서브타입만 사용할 수 있다
슈퍼타입은 사용할 수 없다

타입과 관련해 타입스크립트 형태 (객체와 클래스) 는 그들의 프로퍼티 타입에 공변한다고 말한다
(공변성)

공변은 가변성의 네 종류중 하나다

  • 불변: 정확한 타입 T 를 원함
  • 공변: T 이거나 T 의 서브타입을 원함
  • 반변: T 이거나 T 의 슈퍼타입을 원함
  • 양변: 공변 혹은 양변이길 원함

타입스크립트에서 모든 복합 타입의 멤버 (객체, 클래스, 배열, 함수, 반환 타입) 는
공변이며 함수 매개변수 타입만 예외적으로 반변이다

모든 언어가 타입스크립트 처럼 동작하지는 않는다

함수 가변성

함수 A 가 함수 B 와 같거나적은 수의 매개변수를 가지며 다음을 만족하면
A 는 B 의 서브타입이다

  1. A 의 this 타입을 따로 지정하지 않으면 A의 this 타입은 B 의 this 타입과 같거나 슈퍼타입
  2. A 의 각 매개변수는 B의 매개변수와 같거나 B 의 슈퍼타입
  3. A 의 반환타입은 B 의 반환 타입과 같거나 B의 서브타입

반환타입만 조건이 반대다
객체, 배열, 유니온 등과 달리 함수에서는 반환 타입을 포함한 모든 컴포넌트가
A <: B (A 는 B와 같거나 B의 서브타입) 를 만족하지 않는다

객체의 경우 A <: B 관계가 성립된다
슈퍼타입의 정보는 서브타입이 갖고있지만
서브타입의 정보는 슈퍼타입이 갖고있지 않다

함수의 경우

class Animal {}

class Bird extends Animal {
	chirp() {}
}

class Crow extends Bird {} 

function chirp(bird: Bird): Bird {
  	bird.chirp()
	return bird
}

chirp(new Animal) // error (슈퍼타입은 사용할 수 없다)

function clone(f: (b: Bird) => Bird): void {}

function birdToBird(b: Bird): Bird {}

clone(birdToBird) // ok

function birdToCrow(b: Bird): Crow {}

clone(birdToCrow) // ok

function birdToAnimal(b: Bird): Animal {}

clone(birdToAnimal) // 에러 TS2345

birdToAnimal 은 에러간 난다

클론 함수가 다음과 같을 떄
적어도 babyBird 는 chirp() 를 사용할 수 있어야 한다
그러려면 슈퍼타입인 Animal 타입은 사용할 수 없다

function clone(f: (b: Bird) => Bird): void {
	let parent = new Bird
    let babyBird = f(parent)
    babyBird.chirp()
}

함수의 반환 타입은 공변을 만족해야 한다

profile
떠돌이 생활을 하는. 실업자는 아니지만, 부랑 생활을 하는

0개의 댓글