티스토리 뷰

do/typescript

typescript 5

dooo.park 2019. 4. 16. 13:59

할당가능(assignable)

두 가지 다른 타입 A와 B에 대해, 모든 A 타입의 값을 B 타입의 값으로도 취급할 수 있는가?

type OneDigitOdd = 1 | 3 | 5 | 7 | 9;
const three: OneDigitOdd = 3;
const num: number = three;

number 타입의 값에 OneDigitOdd 타입의 값을 할당한다. 

OneDigitOdd 타입이 가질 수 있는 값인 1, 3, 5, 7, 9 는 모두 number 에 속한다. 

OneDigitOdd 타입의 모든 값이 number 타입의 값이므로 위의 코드는 오류 없이 실행 된다. 

즉, OneDigitOdd 타입은 number 타입에 할당 가능(assignable)하다. 

const four: number = 4;
const oneDigitOdd: OneDigitOdd = four; 
// error TS2322: Type 'number' is not assignable to type 'OneDigitOdd'.

number 타입은 1, 3, 5, 7, 9 이외의 다른 값 또한 가질 수 있다. 

예를 들어, const four: number = 4는 OneDigitOdd 타입의 값으로 허용되지 않는다. 

number 타입의 값이지만 OneDigitOdd 값으로 취급할 수 없는 값이 존재하므로, 

number 타입은 OneDigitOdd 타입에 할당 불가능하다.


객체 타입의 호환성

타입 A와 B에 대해, A가 B에 할당 가능하려면 다음 두 기준을 만족해야 한다.

① B 타입의 모든 필수 멤버에 대해, A 에도 같은 이름의 멤버가 존재하는가?

② B 타입과 A 타입에 동시에 존재하는 멤버 m에 대해, A.m 의 타입을 M, B.m의 타입을 M'라 하자. 

    이 때, 모든 m에 대해서 M이 M'에 할당 가능한가?

 

두 질문에 대한 답이 모두 “예”라면 A는 B에 할당 가능하다. 

반대로 하나라도 만족되지 않으면, A는 B에 할당 불가능하다. 


구조적 타입 시스템

타입스크립트에서는 두 타입의 구조(structure)만을 비교하여 호환성을 결정한다. 

어떤 타입이 다른 타입이 갖는 멤버를 전부 가지고 있다면 그걸로 충분하며, 

두 타입이 호환되는 타입이라는 명시적인 표시는 필요하지 않다. 

이렇게 동작하는 타입 시스템을 구조적 타입 시스템(structural type system)이라 부른다. 


반대되는 개념으로는 C++, Java 등의 언어가 채택한 노미널 타입 시스템(nominal type system)이 있다. 

노미널 타입 시스템을 갖는 언어에서는 특정 키워드를 통해 서로 호환 가능하다고 명시적으로 표현 된 타입 간의

할당만이 허용된다.

 

객체 리터럴과 과잉 속성 검사

interface Color { 
  R: number; 
  G: number; 
  B: number; 
} 

const white: Color = { 
  R: 255, 
  G: 255, 
  B: 255, 
  A: 1 
}; 

변수 white는 Color 타입을 갖는다. 

white에 할당하려는 객체는 R, G, B 세 멤버를 모두 갖고 있고, 세 멤버의 모두 number 타입이다. 

구조적 타입 검사에 의하면 이 할당에는 아무런 문제가 없어야 한다.

하지만 위의 할당은 에러가 발생한다. 

error TS2322: Type '{ R: number; G: number; B: number; A: number; }' is not assignable to type 'Color'. 
  Object literal may only specify known properties, and 'A' does not exist in type 'Color'.

객체 리터럴은 알려진 속성(known property)만을 가질 수 있는데, 

Color 타입에는 A 속성이 존재하지 않으므로 할당이 불가능하다는 메시지이다. 

하지만 분명 할당을 받는 쪽의 타입만이 중요하고, 추가적인 멤버를 갖는건 상관 없다고 했었다.

이러한 현상이 발생하는 이유는 할당하는 값이 변수나 표현식이 아닌 객체 리터럴이기 때문이다. 

객체 리터럴을 할당하는 경우에는 그 리터럴이 알려지지 않은 속성(unknown property), 

즉 할당 받는 타입에 존재하지 않는 속성을 포함한다면 타입 에러가 발생한다.

위의 코드를 객체 리터럴이 아닌 변수를 할당하도록 바꾸면 에러는 사라진다.

interface Color { 
  R: number; 
  G: number; 
  B: number; 
} 

const someColor = { 
  R: 255, 
  G: 255, 
  B: 255, 
  A: 1 
}; 
const white: Color = someColor; 

객체 리터럴에 대해서만 알려지지 않은 속성은 없는지 추가적으로 시행하는 검사를 과잉 속성 검사(excess property checking)라 부른다.

 

과잉 속성 검사는 프로그래머의 실수를 막기 위해 존재한다. 

어떤 타입의 값에 객체 리터럴을 직접 할당하는 경우, 만약 해당 타입에 정의되지 않은 멤버는 

오타 등의 실수로 인해 존재할 확률이 높다고 가정하는 것이다. 

interface SquareConfig { 
  width?: number; 
  color?: string; 
} 
const squareConfig: SquareConfig = { 
  width: 100, 
  colour: red 
}; 

구조적 타입 시스템의 원칙에 따르면 위 할당에는 문제가 없다. 

하지만 할당 시점에 딱 한 번만 사용될 객체 리터럴에 colour라는 알려지지 않은 속성이 존재한다면, 

color를 타이핑하려다 오타가 난 경우가 대부분일 것이다. 

과잉 속성 검사를 시행해서 이런 흔하게 예상되는 오류를 컴파일러가 잡아 줄 수 있다.


함수 타입의 호환성

 

매개변수 수가 같은 경우
함수 타입 간의 호환성을 판단하려 할 때, 가장 간단한 경우는 두 함수의 매개변수의 수가 같은 경우다. 

할당을 받는 함수의 타입을 Target, 할당하려는 함수의 타입을 Source라 하자.

let source: Source; 
const target: Target = source; 

Target이 Source에 할당 가능한지를 보기 위해선 다음 두 질문에 답해야 한다.

① Target과 Source의 모든 매개변수 타입에 대해, Source의 매개변수 타입이 Target의 매개변수 타입에 할당 가능한가?
② Target의 반환 타입이 Source의 반환 타입에 할당 가능한가?

두 질문에 대한 답이 모두 “예”라면, Target은 Source에 할당 가능하다. 

직관적으로 선택 매개변수와 필수 매개변수는 호환성을 판단할 때 다르게 취급되는 게 맞아 보인다. 

하지만 타입스크립트에서는 매개변수가 선택인지 필수인지는 함수 타입의 호환성 판단에 영향을 주지 않는다.

할당 가능한 경우

type Sum = (sumFirst: number, sumSecond: number) => number; 
type Multiply = (mulFirst: number, mulSecond: number) => number; 

① 모든 매개변수 타입은 number로, 서로 할당 가능하다.
② Multiply의 반환 타입인 number는 Sum의 반환 타입인 number에 할당 가능하다.

따라서 Sum은 Multiply에 할당 가능하다.

const sum: Sum (sumFirst: number, sumSecond: number) => { 
  return sumFirst + sumSecond; 
}; 
const multiply: Multiply = sum; // ok 

할당 불가능한 경우

interface Animal { animalProp: string }; 
interface Dog extends Animal { dogProp: number }; 

let f = (animal: Animal) => animal.animalProp; 
let g = (dog: Dog) => { doSomething(dog.dogProp) }; 

f = g; 

① 할당받는 함수의 매개변수 타입 Animal은 할당하는 함수의 매개변수 타입 Dog에 할당 불가능하다. 
만족되지 않는 기준이 있으므로 g는 f에 할당할 수 없다. 

매개변수 수가 다른 경우

type Login = (id: string) => Response; 
type LoginWithToken = (id: string, token: string) => Response; 

할당하는 함수의 매개변수 수가 더 많은 경우
loginWithToken은 할당받는 함수 login에 비해 token: string 이라는 매개변수를 추가적으로 갖고 있다. 

const loginWithToken: LoginWithToken = (id: string, token: string) => { /* ... */ }; 
const login: Login = loginWithToken; 

이런 경우는 할당이 불가능하다. 

할당받는 함수의 매개변수 수가 더 많은 경우

login은 할당받는 함수인 loginWithToken에 비해 매개변수 수가 하나 모자라다. 

const login: Login = (id: string) => { /* ... */ }; 
const loginWithToken: LoginWithToken = login; 

이런 경우 초과된 매개변수는 무시된다. 

그리고 매개변수 수가 같을 때와 동일한 알고리즘으로 호환성을 판단한다. 

초과 매개변수인 token: string 을 제외하고 첫 번째 매개변수는 동일한 타입을 가지므로 할당은 문제 없이 진행된다.


클래스의 호환성

클래스의 호환성 비교는 기본적으로 객체 호환성 비교와 비슷하게 진행된다. 

★ 스태틱 멤버 및 생성자는 호환성 비교에 영향을 주지 않는다. 

 

다음 코드에서 이루어지는 두 할당은 두 클래스의 생성자 타입 시그니처가 다름에도 문제 없이 진행된다.

class Animal { 
  feet: number; 
  constructor(name: string, numFeet: number) { } 
} 

class Size { 
  feet: number; 
  constructor(numFeet: number) { } 
} 

let a: Animal; 
let s: Size; 
a = s; // ok 
s = a; // ok

private 및 protected 멤버

public 멤버를 비교할 때에는 객체 속성을 비교할 때와 마찬가지로 이름이 같은지, 타입이 호환 되는지만 따진다. 

하지만 private 멤버와 protected 멤버는 조금 특별하게 처리된다. 

 

private 및 protected 속성은 이름이 같다고 해도 다른 클래스로부터 정의된 멤버라면 호환이 불가능하다. 

class FacebookUser { 
  constructor (id: string, private password: string) {} 
} 

class TwitterUser { 
  constructor (id: string, private password: string) {} 
} 

let twitterUser: TwitterUser; 
let facebookUser: FacebookUser; 
twitterUser = facebookUser; 

TwitterUser 타입과 FacebookUser 타입은 모두 private password: string 멤버를 갖는다. 

이름은 같지만 두 속성은 서로 다른 클래스에서 정의된 private 멤버다. 

위와 같은 할당을 시도한다면 다음 타입 에러가 발생한다.

// error TS2322: Type 'FacebookUser' is not assignable to type 'TwitterUser'. 
//   Types have separate declarations of a private property 'password'.

제너릭 호환성

제너릭의 호환성은 기본적으로 객체의 호환성과 비슷하게 동작한다. 

모든 타입 변수가 어떤 타입인지 알려진 경우와 그렇지 않은 두 가지 경우가 있다. 

모든 타입 변수가 어떤 타입인지 알려진 경우

interface NotEmpty<T> {
  data: T;
}

let x: NotEmpty<number>;
let y: NotEmpty<string>;

x와 y는 각각 NotEmpty와 NotEmpty 타입을 가진다. 

하지만 제너릭 인터페이스의 정의를 보면
NotEmpty 는 { data: number } 로
NotEmpty은 { data: string } 으로 
고쳐 씀으로써 타입 변수를 완전히 제거할 수 있다. 

 

따라서 NotEmpty가 NotEmpty에 할당 가능한지 여부를 판단하는 일은 객체 타입간의 할당 여부를 판단하는 것과 같다.
이 경우 number는 string에 할당 불가능하므로 할당을 시도한다면 타입 에러가 날 것이다.

어떤 타입인지 알려지지 않은 타입 변수가 있는 경우

제너릭 타입의 호환성을 판단하는 시점에 타입 변수가 알려져 있지 않은 경우

const identity = function(x: T): T { 
  // ... 
}; 

const reverse = function(y: U): U { 
  // ... 
}; 

identity와 reverse 함수의 타입에는 타입 변수가 남아 있다. 

이럴 때는 남아 있는 타입 변수를 모두 any 타입으로 대체하고 호환성을 판단한다. 

 

아래와 같은 할당은 허용된다. 

identity = reverse; 

타입 변수 T를 any로 대체한 (x: any) => any 와 

타입 변수 U를 any로 대체한 (y: any) => any 는 서로 할당 가능한 타입이기 때문이다.


열거형의 호환성

열거형의 호환성은 객체 타입이 연관된 경우에 비해 간단하다. 

다른 열거형으로부터 유래된 값끼리는 호환되지 않는다.

enum Status { Ready, Waiting } 
enum Color { Red, Blue, Green } 
let status: Status = Status.Ready; 
status = Color.Green; // error 

숫자 열거형 값은 number에, 문자열 열거형 값은 string에 할당 가능하다.

enum MyEnum { 
  Zero, 
  One = 1, 
  Name = 'a' 
} 
const zero: number = MyEnum.Zero; 
const one: number = MyEnum.One; 
const name: string = MyEnum.Name;

참고사이트

https://ahnheejong.gitbook.io/ts-for-jsdev

'do > typescript' 카테고리의 다른 글

typescript 4  (0) 2019.04.14
typescript 3  (0) 2019.04.12
typescript 2  (0) 2019.04.10
typescript 1  (0) 2019.04.09
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함