[Typescript] Type Compatibility 타입 호환성

eunn·2020년 1월 7일
0

기본 타입

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

OneDigitOdd 타입이 가질 수 있는 값인 1, 3, 5, 7, 9는 모두 number에 속한다. 즉, OneDigitOdd 타입은 number 타입에 할당 가능(assignable) 하다.

반면, 아래의 코드처럼 4OneDigitOdd의 값에 해당하지 않으므로 할당이 불가능하다.

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

객체 타입

interface User {
  name: string;
  age: number;
}
interface Pet {
  name: string;
  species?: string;
}
const user: User = {name: '집사', age: 10};
const pet: Pet = {name: '나비'};
const pet2: Pet = user;  (o)
const user2: User = pet; (x)

타입스크립트에서는 두 타입의 구조만을 비교하여 호환성을 결정한다.
User는 Pet 이 가지고 있는 name을 갖기 때문에 호환이 가능하지만,
Pet은 User 가 가지고 있는 nameage 모두를 충족할 수 없기 때문에 호환이 불가능하다. 이렇게 동작하는 타입 시스템을 구조적 타입 시스템(structural type system)이라 부른다.

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


변수 whiteColor 타입을 갖는다. 그리고 white에 할당하려는 객체는 R, G, B 세 멤버를 모두 갖고 있고, 세 멤버 모두 number 타입이다. 따라서 구조적 타입 검사에 의하면 이 할당에는 아무런 문제가 없어야 하지만 A 는 error 를 발생시킨다.
그 이유는 객체 리터럴을 할당하는 경우에는 그 리터럴이 알려지지 않은 속성, 즉, A와 같은 할당 받는 타입에 존재하지 않는 속성을 포함하면 타입 에러가 발생한다.

하지만, 아래 코드처럼 객체 리터럴이 아닌 변수를 할당하도록 바꾸면 에러는 사라진다.
이처럼 객체 리터럴에 알려지지 않은 속성이 있는지 추가적으로 시행하는 검사를 과잉 속성 검사(excess property checking)라 부른다.

과잉 속성 검사가 존재하는 이유

과잉 속성 검사는 프로그래머의 실수를 막기 위해 존재한다. 어떤 타입의 객체 리터럴을 직접 할당하는 경우, 만약 해당 타입에 정의되지 않은 멤버는 오타 등의 실수로 인해 존재할 확률이 높다고 가정하는 것이다.

함수 타입

매개변수 수가 같은 경우

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

모든 매개변수 타입이 number로, 아래의 코드와 같이 서로 할당이 가능하다.

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) => dog.dogProp;

f = g; // error

매개변수 수가 다른 경우

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

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

const loginWithToken: LoginWithToken = (id: string, token: string) => 'a';
const login: Login = loginWithToken;

loginWithToken 은 할당받는 함수 login 에 비해 token: string 이라는 매개 변수를 추가적으로 갖고 있다.
이런 경우는 할당이 불가능하다. 만약 이 할당을 허용한다고 생각한다고 생각해 보면, 아래와 같이 호출할 것이다.

login('myId');

이는 loginWithToken 함수를 token 인자 없이 호출하는 셈이다.
loginWithToken 함수 내에서 tokenstring 타입이라 생각하고 사용했다면, string 이 필요한 자리에 undefined 값이 넘어와 런타임 에러가 발생할 것이다.
따라서, 이런 할당은 허용되지 않는다.

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

const login: Login = (id: string) => 'a';
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 = new Size(3);
a = s; // ok
s = a; // ok

private 및 protected 멤버

public 멤버를 비교할 때에는 객체 속성을 비교할 때와 마찬가지로 이름이 같은지, 타입이 호환 되는지만 따진다. 하지만 private 멤버와 protected 멤버는 조금 다르게 처리된다. privateprotected 속성은 이름이 같다고 해도 다른 클래스로부터 정의된 멤버라면 호환이 불가능하다.

class FacebookUser {
  contructor (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멤버다. 따라서 위와 같은 할당을 시도하면, 다음 타입 에러가 발생한다.

Type 'FacebookUser' is not 
![스크린샷 2020-01-05 오후 1.26.59.png](https://velog.velcdn.com/post-images%2Feunn%2Fa2a4b830-2f73-11ea-a9bd-5da04f8bae2b%2F-2020-01-05-1.26.59.png)
assignable to type 'TwitterUser'.
Types have separate declarations of a private property 'password'.(2322)

제네릭

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

interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string> = {};

x = y; // ok 

위의 경우 xy의 구조가 인수 타입를 차별화된 방식으로 사용하지 않기 때문에 호환 가능하다.

interface NotEmpty<T> {
  data: T;
}

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

이 경우,

  • NotEmpty<number>{ data: number }
  • NotEmpty<string>{ data: string } 으로
    고쳐 씀으로써 타입 변수를 완전히 제거할 수 있다. 따라서, 이 경우는 객체 타입간의 할당 여부를 판단하는 것과 같으므로, numberstring 을 할당하는 것은 불가능하다.

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

let identity = function<T>(x: T): T {
  // ...
};
let reverse = function<U>(y: U): U {
  // ...
};

이 때, identityreverse 함수의 타입에는 타입 변수가 남아 있다. 이럴 때에는 아직 남아있는 변수를 모두 any 타입으로 대체하고 호환성을 판단 한다.
아래와 같은 할당은 허용한다.

identity = reverse;

타입변수 Tany 로 대체한 (x: any) => any 와 타입 변수 Uany 로 대체한 (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 = '꽃'
}
const zero: number = MyEnum.Zero;
const one: number = MyEnum.One;
const name: string = MyEnum.Name;
profile
사람-컴퓨터와 소통하고 싶은 병아리 개발자입니다🐥

0개의 댓글