-
[TS] Typescript의 타입 검사Javascript, Typescript 2021. 11. 1. 20:58반응형
타입스크립트의 공식문서에서 말하기를, Typescipt의 타입 검사는 해당 값의 ‘형태’에 관심을 갖는 덕 타이핑(구조적 타이핑)을 따른다고 한다.
One of TypeScript’s core principles is that type checking focuses on the shape that values have. This is sometimes called “duck typing” or “structural typing”.
Typescript의 이 타입 검사 시스템은 개발자에게 편리함을 줄 때도 있지만, 개발자가 예상하지 못한 곳에서 버그를 일으킬 수도 있다.
덕 타이핑(Duck typing) 또는 구조적 타이핑(Structural typing)이라고 하는 타입스크립트의 타입 체킹 방법에 대해 알아보고 이에 따른 일어날 수 있는 문제와 해결방법을 알아보려고 한다.
Duck Typing
interface Person { name: string; age: number; } interface Animal { name: string; age: number; owner: Person; } const person: Person = { name: 'Park', age: 25 } const animal: Animal = { name: 'White', age: 4, owner: person } let me: Person; me = animal; // error가 발생하지 않음
Java나 C#같은 언어를 사용하다가 타입스크립트를 처음 사용할 때 굉장히 당황스러울 수 있는 코드다. 명시적 타이핑(Nominal typing) 방식을 채택한 Java와 C# 컴파일러의 타입 검사에서는 에러가 발생하는 코드이기 때문이다.
명시적 타이핑 방식에서는 할당하려는 타입의 적합성을 검사할 때, 타입의 이름이 같은지(또는 명시적으로 선언된 서브타입인지) 검사하기 때문에 Person 타입의 변수에 Animal 타입의 변수는 할당할 수가 없다.
하지만 덕 타이핑(Duck typing) 방식을 채택한 타입스크립트는 위 코드를 에러 없이 처리한다.
덕 타이핑이라는 말은 "만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다." 라는 명제로 정의되는 덕 테스트(Duck test)에서 유래된 프로그래밍 용어이다. B라는 타입이 A라는 타입의 속성을 가지고 있으면 A타입이라고 보는 것이다.
Person 타입을 덕 타이핑 관점으로 해석하면, "만약 어떤 타입이 name이라는 string 타입 프로퍼티가 있고, age라는 number 타입 프로퍼티가 있다면 그것을 Person 타입이라고 볼 것이다" 로 비유할 수 있다.
따라서 타입스크립트 컴파일러는 animal이라는 변수가 name이라는 string 타입 프로퍼티가 있고, age라는 number 프로퍼티가 있기 때문에 Person변수에 할당이 가능하다고 해석하는 것이다.
이러한 타입스크립트의 타입 체킹 특성 때문에, 함수 호출의 매개변수로 다른 타입의 변수를 전달할 때도 컴파일러의 타입 검사를 통과할 수 있다.
// 위 코드에서 이어짐 function introduce(person: Person) { console.log(`My name is ${person.name}!`) } introduce(animal); // error가 발생하지 않음
타입스크립트로 개발을 하다보면 어? 이게 왜 오류가 안나지? 하는, 한번쯤 일어날 수 있는 상황이다. 해당 변수가 Animal 타입인 것을 확실히하지 않으면 자칫 Person 타입이라고 착각하고 버그를 생산할 가능성이 있다.
그래서 타입스크립의 in 오퍼레이터를 이용해서 person 변수에 Person 타입에만 있는 'owner' 프로퍼티가 있는지 검사하는 Type Guard를 통해서 런타임에 오류가 나게끔 코드를 다음처럼 수정할 수가 있다.
function introduce(person: Person) { if ('owner' in person) { throw new Error(`${person} is not Person!`); } console.log(`My name is ${person.name}!`); }
이렇게 조건문을 통해 타입을 좁히는 방식을 타입스크립트 공식문서에서는 Narrowing 이라고 한다.
하지만 우리가 Javascript가 아닌 Typescript를 사용하는 이유 중 하나는 이렇게 런타임에 변수의 형태에 대한 검사를 하는 것이 아닌, 컴파일 시점에 타입 검사를 통해 런타임 버그를 방지하는 것이기 때문에 타입스크립트의 이러한 타입 검사가 불편하게 느껴진다.
Person타입에 Animal을 할당한다던가, Person타입 매개변수에 Animal 타입을 전달하는 경우 컴파일 시점에 오류가 나기를 바랄 때가 있다.
이럴때 사용하는 방법 중 대표적으로 Branding 방법이 있다. 타입에 상표를 붙여서 해당 타입임을 명시함으로써 명시적 타이핑 방식처럼 타입 검사를 하는 방법이다.
Branding
interface Person { _brand: 'person'; // brand를 명시해준다 name: string; age: number; } interface Animal { _brand: 'animal' // brand를 명시해준다 name: string; age: number; owner: Person; } const person: Person = { _brand: 'person', name: 'Park', age: 25 }; const animal: Animal = { _brand: 'animal', name: 'White', age: 4, owner: person }; let me: Person; me = animal; // Error: Types of property '_brand' are incompatible.
_brand 변수의 값이 인터페이스에서 정의한 값과 다르기 때문에 컴파일 시점에 서로 다른 타입의 할당을 막을 수 있게 되었다. 단점이라면 불필요한 _brand라는 프로퍼티가 생기는 것과, 악의적으로 animal 오브젝트의 _brand를 ‘person’이라고 지정하는 경우는 막을 방법이 없다는 것이다.
하지만 Branding 방법에 타입스크립트의 Type Predicates를 함께 이용하는 타입 가드를 사용한다면, 다음과 같이 프로퍼티와 메소드가 모두 같은 타입도 다른 타입으로 구분해낼 수 있다.
interface Person { name: string; age: number; } type Adult = Person & { _brand: 'adult' } function isAdult(person: Person): person is Adult { return person.age > 20; } function drive(person: Adult) { // drive } function main() { const person = { name: 'Kim', age: 27 }; if (isAdult(person)) { drive(person); } drive(person); // Error!! }
Person 타입을 매개변수로 하는 isAdult() 라는 함수를 정의하고, 타입스크립트 컴파일러에게 이 함수의 return값이 참이면 매개변수로 주어진 변수는 Adult 타입이라고 판단하게 만들었다.
Adult 타입에 Branding이 되어있지 않았다면 isAdult()를 통과하지 못한 person 오브젝트도 drive 함수의 매개변수로 사용할 수 있었을 것이다(name과 age를 프로퍼티로 가지기 때문에). 이러한 타입 체크는 컴파일된 코드에는 존재하지 않기 때문에, 런타임에 실제로는 어떤 비교도 수행하지 않아 런타임 오버헤드가 없다는 장점도 있다.
또한 Branding 방법은 number나 string같은 기본 타입에도 사용할 수 있다는 장점이 있다.
Conclusion
타입스크립트 개발자들이 타입스크립트에 명시적 타이핑을 통한 엄격한 타입 체크가 아닌 구조적 타이핑을 통한 타입 체크를 선택한 이유가 있을것이다(C#을 만든 분들임에도..!). 아마 기존 Javascript 개발자들이 받아들이기에는 좀 더 유연한 타입 체킹이 필요했기 때문이라고 생각해서 구조적 타이핑을 채택하지 않았나 싶다.
타입스크립트의 구조적 타이핑은 잘 이해하고 있으면 컴파일 타임 오류 없이 유연한 코드를 작성할 수 있다는 장점이 있지만, 제대로 이해하고 있지 않다면 런타임에 예상치 못한 버그가 나올 가능성도 있으니 컴파일러의 타입 체킹 결과에 대해 한 번 더 생각해보고 코드를 작성할 수 있도록 해야겠다.
References
반응형'Javascript, Typescript' 카테고리의 다른 글
[TypeORM] repository.save()의 동작과 upsert() (5) 2021.12.19 [TypeORM] Relation 관계에서 Join을 하는 방법들 (3) 2021.12.08 [JS] Trailing Commas에 대한 고찰 (feat.ESLint) (0) 2021.12.05 [JS] 비동기 작업들의 순차실행과 병렬실행 (0) 2021.11.09 [TS] Typescript의 메소드 오버로딩 (0) 2021.11.04