해당 페이지를 번역 및 요약함. https://effectivetypescript.com/2021/05/06/unsoundness/
2부 : 타입스크립트의 타입 불건전성에 대하여 2 : 공식 예제 코드 살펴보기 (tistory.com)
타입스크립트는 타입 안정성을 위해 사용한다.
혹은 객체 지향을 강화하게 사용한다.
그런데 타입 안정성을 위해 사용하는 타입스크립트에는 기본적으로 허용되는 타입 불건전성(unsoundness)가 있다.
대표적으로는 any, 타입 단언, 함수 오버로딩이 있다.
따라서 해당 부분을 의도적으로 주의할 필요가 있다.
https://www.typescriptlang.org/ko/docs/handbook/type-compatibility.html
TypeScript의 타입 시스템은 컴파일 시간에 확인할 수 없는 특정 작업을 안전하게 수행할 수 있습니다. 타입 시스템이 이런 특성을 갖고 있을 때, “건전”하지 않다고 말합니다. TypeScript에서 건전하지 못한 곳을 허용하는 부분을 신중하게 고려했으며, 이 문서 전체에서 이러한 상황이 발생하는 곳과 유발하는 시나리오에 대해 설명합니다.
해당 글의 작성자는 이렇게 말한다.
대략적으로 말하면 모든 기호의 정적 유형이 런타임 값과 호환되도록 보장되는 경우 언어는 건전합니다.
const x = Math.random();
// type is number
즉 x:number이고, x의 값이 math.random()이면 x는 건전하다.
더 정확한 타입은 [0, 1)이라 할 수 있으나, 이정도면 충분하다.
타입스크립트는 그 이상으로 값을 더 정확하게 타이핑할 방법도 없으며, 건전성은 Precision(정밀도)에 대한 이야기다.
여기에 불건전성의 예시가 있다.
예시 : T[]
const xs = [0, 1, 2]; // type is number[]
const x = xs[3]; // type is number, but value is undefined;
하지만 건전성이 타입스크립트의 목적은 아니다. 타입스크립트는 기본 자바스크립트와의 호환성을 우선한다.
우리는 타입스크립트의 불건전성을 이해하고, 런타임 충돌 문제를 피하는 것이 좋다.
많은 프로그래밍 언어는 스스로 sound하다 주장한다. 하지만 자바와 스칼라도 sound하지 않다.
예시 : any
any는 의도적으로 피할 수 있으나, any를 리턴하는 몇몇 기능들을 사용할 때는 피하기 어렵다. (JSON.parse)
JSON.parse("{a:1}") // 무조건 애니를 리턴함.
function alertNumber(x: number) {
alert(x.toFixed(1)); // static type of x is number, runtime type is string
}
const num: any = 'forty two';
alertNumber(num);
// no error, throws at runtime:
// Cannot read property 'toFixed' of undefined
effective typescript의 5장에서는 any를 어떻게 안전하게 사용하는지에 대해 다루고 있다고 한다 (unknown을 대안으로).
궁금하면 살펴보자.
예시 : Type Assertion (not the "cast", rant on this terminology):
꽤 자주 사용하게 되는 기능이다.
function alertNumber(x: number) {
alert(x.toFixed(1));
}
const x1 = Math.random() || null; // type is number | null
// 주 : random이 0이면 null이 된다.
alertNumber(x1);
// ~~ ... Type 'null' is not assignable to type 'number'.
alertNumber(x1 as number); // type checks, but might blow up at runtime. 주 : 정적 타입 체크는 피해가지만. 런타임에 오류 발생 가능.
위에 주석에도 적어뒀는데, Math.random이 0 이면 null이 되어 에러가 발생한다.
타입 단언은 api 호출 시에도 많이 사용한다.
const response = await fetch('/api/fun-fact');
const fact = await response.json() as FunFact;
타입 단언 없애는 방법 1 : if로 축소
const x1 = Math.random() || null; // type is number | null
if (x1 !== null) {
alertNumber(x1); // ok, x1's type is number
}
타입 단언 없애는 방법 2 : 타입 가드
function isFunFact(data: unknown): data is FunFact {
return data && typeof data === 'object' && 'fact' in data /* && ... */;
}
const response = await fetch('/api/fun-fact');
const fact = await response.json();
if (!isFunFact(fact)) {
throw new Error(`Either it wasn't a fact or it wasn't fun`);
}
// type of fact is now FunFact!
타입 가드를 대신할 방법들
Zod , typescript-json-schema , crosswalk
예시 : 객체와 배열 조회
매우 자주 사용한다.
const xs = [1, 2, 3];
const x = xs[3]; // static type is number but runtime type is undefined.
alert(x.toFixed(1));
// no error, throws at runtime:
// Cannot read property 'toFixed' of undefined
인덱스 타입 객체도 마찬가지다.
type IdToName = { [id: string]: string };
const ids: IdToName = {'007': 'James Bond'};
const agent = ids['008']; // static type is string but runtime type is undefined.
const xs = [1, 2, 3];
const x3 = xs[3]; // static type is number | undefined
alert(x3.toFixed(1));
// ~~ Object is possibly 'undefined'.
const x2 = xs[2]; // static type is number | undefined
alert(x2.toFixed(1));
// ~~ Object is possibly 'undefined'.
몇몇 문법을 사용하는 경우는 똑똑하게 작동함.
const xs = [1, 2, 3];
for (const x of xs) {
console.log(x.toFixed(1)); // ok
}
const squares = xs.map(x => x * x); // also ok
아래와 같은 방법도 있는데 이 방법의 단점은 딱히 말하지 않겠음. (주 : 진짜 undefined가 가능함)
const xs: (number | undefined)[] = [1, 2, 3];
const x3 = xs[3]; // static type is number | undefined
alert(x3.toFixed(1));
// ~~ Object is possibly 'undefined'.
type IdToName = { [id: string]: string | undefined };
const ids: IdToName = {'007': 'James Bond'};
const agent = ids['008']; // static type is string | undefined
alert(agent.toUpperCase());
// ~~~~~ Object is possibly 'undefined'.
다른 방법은 배열 , 속성 조회를 피하는 것이다. (객체 참조로 전달)
interface MenuProps {
menuItems: {[id: string]: MenuItem};
onSelectItem: (menuItem: MenuItem) => void;
}
예시 : 부정확한 타입 정의
@types로 시작하는 라이브러리들은 js로 작성되어 tsc로 추론된 파일들일 경우가 대부분이다.
애초부터 타입스크립트로 작성한게 아닌 이상 js 동적 타입 시스템의 단점을 가져온다.
해당 버그들은 매우 빨리, 자주 수정되어 보기 어렵지만 발생할 경우 치명적이다.
또한, 역사적인 이유로 부정확한 타입을 추론하는 경우도 있다. https://github.com/microsoft/TypeScript/pull/28553
예시 : 버라이언스와 배열 - 파라미터 변경에 의한 문제
class Animal {}
class Mammal extends Animal {
isMammal=true;
}
class Cat extends Mammal {
isCat=true;
isDog=false;
}
class Dog extends Mammal {
isDog=true;
isCat=false;
}
function addDogOrCat(arr: Animal[]) {
arr.push(Math.random() > 0.5 ? new Dog() : new Cat());
}
function getMammals(): Mammal[] {
return [];
}
// B
function hasCat(arr: Animal[]) {
return arr.some(e => e instanceof Cat);
}
const x: Mammal[] = getMammals();
const y = hasCat(x);
// C
const z: Cat[] = [new Cat()];
addDogOrCat(z); // // Sometimes puts a Dog in a Cat array, sad!
가장 좋은 해결책은 파라미터를 변경하지 않는 것
function addDogOrCat(arr: readonly Animal[]) {
arr.push(Math.random() > 0.5 ? new Dog() : new Cat());
// ~~~~ Property 'push' does not exist on type 'readonly Animal[]'.
}
아래와 같이 작성한다.
function dogOrCat(): Animal {
return Math.random() > 0.5 ? new Dog() : new Cat();
}
const z: Cat[] = [new Cat(), dogOrCat()];
// ~~~~~~~~~~ error, yay!
// Type 'Animal' is missing the following properties from type 'Cat': ...
함수 인자에서 부수효과 발생
타입 체크는 다 피하지만, 오류가 발생할 수 있는 경우.
(파라미터로 전달한 객체에서 부수효과를 발생시키는 경우 - 이 경우는 자바같은 언어도 마땅한 답이 없다.)
interface FunFact {
fact: string;
author?: string;
}
function processFact(fact: FunFact, processor: (fact: FunFact) => void) {
if (fact.author) {
processor(fact);
document.body.innerHTML = fact.author.blink(); // ok
}
}
// 런타임 오류 발생!
processFact(
{fact: `Peanuts aren't actually nuts`, author: 'Botanists'},
f => delete f.author
);
파라미터를 불변으로 처리하여 해결한다.
function processFact(fact: FunFact, processor: (fact: Readonly<FunFact>) => void) {
// ...
}
processFact(
{fact: `Peanuts aren't actually nuts`, author: 'Botanists'},
f => delete f.author
// ~~~~~~~~
// The operand of a 'delete' operator cannot be a read-only property.
);
function processFact(fact: FunFact, processor: (fact: FunFact) => void) {
const {author} = fact;
if (author) {
processor(fact);
document.body.innerHTML = author.blink(); // safe
}
}
더 읽어볼 자료들
https://frenchy64.github.io/2018/04/07/unsoundness-in-untyped-types.html
추가 : 윗글에는 없지만 함수 오버로딩도 컴파일러를 속일 수 있다.
function test(a:number):string;
function test(a:string):number;
function test(a: any):string|number{
return typeof a ==='number' ? Number(a) : String(a);
}
'FrontEnd' 카테고리의 다른 글
아폴로 클라이언트로 알아보는 클라이언트 아키텍처 [Apollo Client & Client-side Architecture Basics] (0) | 2022.03.27 |
---|---|
타입스크립트의 타입 불건전성에 대하여 2 : 공식 예제 코드 살펴보기 (0) | 2022.02.27 |
Context API와 React.memo (0) | 2022.02.14 |
Context API(컨텍스트API)는 Dependency Injection(의존성 주입) 수단이다. (1) | 2022.02.11 |
React 컴포넌트 DRY하게 작성하기 : varient (0) | 2022.02.01 |