
"To Type or Not to Type: Quantifying Detectable Bugs in JavaScript" (2017, ICSE)에 기술된 내용 입니다.
자바스크립트는 폭발적으로 성정해 이제는 웹 영역을 넘어서 대규모의 성숙한 프로젝트에서도 사용 되고있다. 자바스크립트는 동적 타입 언어이지만, Facebook의 Flow나 Microsoft의 타입스크립트와 같은 정적 타입 시스템들이 개발되었다. 이 정적 타입 시스템들은 어떤 이점을 제공할까?
저자들은 자바스크립트 프로젝트의 이력을 활용해, 수정된 버그들을 선택하고 해당 수정 직전의 코드를 확인했다.
버그가 있는 코드에 수동으로 타입 어노테이션을 추가하고, Flow와 Typescript가 버그가 있는 코드에서 에러를 보고하는지 테스트했다. 이를 통해 개발자가 버그를 공개 배포 전에 수정할 수 있었는지 확인했고, 에러를 보고한 버그의 비율을 조사했다.
공개된 버그들에 대해 정적 타입 시스템을 평가하는 것은 보수적이다. 개발 과정 중의 버그 검출 효과를 과소평가 하게되고, 코드 검색/자동완성 기능 개선이나 문서화 역할과 같은 다른 이점들은 고려하지 않은 것이다.
이러한 불균형 조건에도 불구하고, 두 정적 타임 시스템 모두 상당한 비율의 공개 버그를 찾아낸다: 모두 15%의 버그를 검출했다.

Airbnb에서는 무려 38%의 버그를 타입스크립트 사용으로 예방할 수 있었다고 한다.
세 회사가 자바스크립트를 위한 정적 타입 시스템을 개발할 만큼 정적 타입을 중요하게 여겼다: Google이 Closure를 공개했고, 그 다음 Microsoft의 TypeScript, 가장 최근에는 Facebook이 Flow를 발표했다.
타입스크립트는 자바스크립트의 슈퍼셋이다.

타입스크립트는 자바스크립트의 모든 기능을 제공하고, 그 위에 추가적인 계층을 제공한다: 타입스크립트의 타입시스템이다.
타입 시스템에는 크게 덕 타이핑, 명목적 타이핑, 구조적 타이핑이 있다. 타입스크립트는 구조적 타이핑 시스템을 따르는데, 각 타입 시스템에 대해 알아보자.

“When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.” - James Whitcomb Riley
오리처럼 걷고, 오리처럼 꽥꽥거린다면, 그것은 오리일 것이다**
덕 타이핑이라는 용어는 James Whitcomb Riley의 “덕 테스트”에서 유래했다. 이는 프로그래밍에서 객체의 타입을 그 객체가 가진 메서드와 속성으로 결정하는 방식을 재치있게 설명한 것이다.

덕 테스트는 귀추법이다. 귀추법은 가정을 선택하는 추론의 한 방법으로써, 만약 사실이라면 관계있는 증거를 가장 잘 설명할 것 같은 가정을 선택하는 방법이다.
덕 타이핑에서는 어떤 객체가 어떤 타입에 요구되는 메서드와 프로퍼티를 가지고 있다면 해당 타입으로 간주한다. 덕 타이핑은 특정 객체가 어떤 타입이 요구하는 구조를 실제로 사용할 수 있는지에 기반해 동등성을 판단하는 방식이라고 볼 수 있다.
추후 알아볼 구조적 타이핑과 유사하지만 차이점이 있다. 덕 타이핑은 런타임에 타입 검사가 이루어진다. 실제로 메서드를 호출하거나 속성에 접근할 때 검사한다. 파이썬이 대표적인 예시다.
명목적 타이핑은 타입의 이름을 기준으로 타입 호환성을 판단한다. 두 타입이 동일한 구조를 가진다 해도 서로 다른 이름을 가지고 있다면 다른 타입으로 간주된다.
명목적 타이핑 예시
struct UserId(String);
struct ProductId(String);
fn process_user_id(id: UserId) {
println!("Processing user id: {}", id.0);
}
fn main() {
let product_id = ProductId(String::from("123"));
process_user_id(product_id); // 컴파일 에러 발생
}
타입스크립트는 구조적 타이핑을 사용한다. 타입의 실제 구조나 정의를 기반으로 타입 호환성을 검사한다. 두 타입의 이름이 달라도 구조가 호환된다면 같은 타입으로 취급된다.
interface Point {
x: number;
y: number;
}
class Coordinate {
constructor(public x: number, public y: number) {}
}
function printPoint(point: Point) {
console.log(`x: ${point.x}, y: ${point.y}`);
}
// Coordinate는 Point와 구조가 같으므로 호환됨
const coord = new Coordinate(10, 20);
printPoint(coord); // 정상 작동!
아래 내용은 Making sense of TypeScript using set theory 글을 번역하고 재가공한 글입니다.
타입 스크립트의 타입 시스템에 대한 이해없이 그냥 사용만 해왔다. 그에 한계를 느겼고 “이펙티브 타입스크립트” 라는 책을 읽고, 각종 자료들을 찾아봤다.
이제는 어느정도 이해했고 그 기반에는 집합 이론이 있다. 타입 스크립트의 타입 시스템에 집합 이론을 대입하면 이해하기 아주 쉬워진다.
수학에서 집합은 어떤 명확한 조건을 만족시키는 서로 다른 대상들의 모임이다. 집합론은 추상적 대상들의 모임인 집합을 연구하는 수학 이론이다.
쉽게 설명하기 위해, 두 개의 프로그래밍 언어(python과 javascript)와 이 언어들을 담을 수 있는 그룹, 즉 일명 집합이 있다고 가정해보자.
총 4개의 프로그래밍 언어 집합을 만들 수 있다:
{ python }{ javascript }{ python, javascript }{}. 이를 ∅ 기호로 표기한다.집합은 종종 "벤 다이어그램(venn diagrams)"으로 그려지며 각 집합은 원으로 표시된다.
집합 A의 모든 요소가 B에도 있는 경우 A는 B의 부분집합이라고 한다.
프로그래밍 언어 세계에서 { python }은 { python, javascript }의 부분집합이지만 { javascript }는 { python }의 부분집합이 아니다. 모든 집합은 자신의 부분집합이고 공집합은 다른 모든 집합 S의 부분집합이다. 공집합의 어떤 항목도 집합 S에서 누락되지 않았기 때문이다.
집합에 정의된 몇 가지 유용한 연산자가 있다:
{ python } ∪ { javascript } = { python, javascript }{ python, javascript } ⋂ { javascript } = { javascript }{ python, javascript } \ { javascript } = { python }타입과 집합론은 밀접한 관계를 가지고 있다. 타입은 본질적으로 자바스크립트 값들의 집합이라고 생각할 수 있다. 이때 전체 집합은 자바스크립트 프로그램이 생성할 수 있는 모든 값이 된다.
집합의 기본 개념을 타입스크립트와 연결해 생각해보면 다음과 같다:
never 타입이다extends 키워드로 표현된다|)으로 표현된다&)으로 표현된다Exclude 타입으로 표현된다Exclude는 유니온 타입에서만 동작한다가장 간단한 타입부터 시작해보자.
true 및 false 다.boolean은 부울 값이다.never 이다.
타입 세계와 집합 세계 사이를 연결해보자.
boolean = true | falsetrue는 boolean의 부분집합/서브타입 이다.never는 공집합이므로 true, false 및 boolean의 부분집합/서브타입 이다.&는 교집합이다.false & true = neverboolean & true = (true | false) & true = truetrue & never = neverExclude는 차집합을 정확하게 계산한다: Exclude<boolean,true> = false|는 합집합이다.true | never = true, boolean | true = boolean이제 약간 까다로운 extends를 다뤄보자.
type A = boolean extends never ? 1 : 0;
type B = true extends boolean ? 1 : 0;
type C = never extends false ? 1 : 0;
type D = never extends never ? 1 : 0;
“extends”가 부분집합으로 읽힐 수 있다는것을 기억한다면 대답을 할 수 있을거다:
null 및 undefined는 각각 하나의 값만 포함하는 집합(타입)이라는 점만 제외하면 boolean과 같다.
never extend null은 유효하다.null & boolean은 never다.
string은 “모든 자바스크립트 문자열” 타입이며 이 모든 문자열에는 리터럴 타입이 포함된다. 불리언 같은 유한 집합과 한 가지 중요한 차이점이 있다. 바로 가능한 문자열 값이 무한하다는 것이다.
집합과 마찬가지로 다양한 방법으로 문자열 타입을 구성할 수 있다.
|을 사용하면 유한한 문자열 집합을 정의할 수 있다.type Country = “de” | “us”;type V = v${string}은 v로 시작하는 문자열이다.리터럴 string 타입과 템플릿 리터럴 타입의 합집합과 교집합을 만들 수 있다. union 타입을 템플릿 리터럴 타입과 교집합할 때, TS는 템플릿에서 리터럴을 필터링할 만큼 똑똑하다.
‘a’ | ‘b’ & `a${string}` = ‘a’
그러나 TS는 템플릿을 병합할 만큼 똑똑하지 않다. 몇몇 string 타입은 타입스크립트에서 표현할 수 없다. Exclude<string, ‘a’> 같은 부정 조건을 표현할 수 없다.
number, symbol 및 bigint에 대한 타입은 “템플릿” 타입 없이 유한 집합으로 제한된다는 점을 제외하면 문자열과 동일한 방식으로 동작한다.

const x: {} = 9가 왜 정상적일까? 이를 이해하지 못한다면 타입스크립트 객체 타입/레코드/인터페이스에 대한 가정은 잘못된 멘탈 모델 위에 구축되어 있는것이다.
먼저, type Sum9 = { sum: 9 } 과 같은 타입이 단일 객체 값 { sum: 9 }와 일치하는 객체에 대한 “리터럴 타입” 처럼 동작할 것이라 생각할 수 있다.
이는 타입스크립트의 동작 방식이 아니다. Sum9 타입은 “9를 얻기 위해 적절한 sum프로퍼티에 액세스할 수 있는 가”를 의미한다.
즉, 타입은 제약 조건과 비슷하다. 타입을 이용하면 타입스크립트가 알 수 없는 date 속성에 대해 불평하지 않고 객체 obj = { sum: 9, date: “2022-09-13” }을 인자로 (data: Sum9) ⇒ number를 호출할 수 있다.
{} 타입은 {} 리터럴 객체에 해당하는 “빈 객체” 타입이 아니라 어떤 속성의 액세스에도 관심없는 타입을 의미한다.
x = 9는 어떤 속성의 액세스에도 관심 없기 때문에 {}를 충족한다. const x: { toString(): string } = 9; 와 같은 방식도 가능하다. autoboxing에 의해 타입스크립트가 원시 타입을 객체로 본다는 것을 의미한다.
그럼 object 타입은 무엇일까? “원시값이 아닌 자바스크립트 객체”를 의미하는 타입이다.

extends는 클래스를 확장한다는 의미의 객체 지향 세계에서 왔지만, 구조적 타이핑 기반인 타입스크립트에서의 extends는 동일하지 않다.
type Extends<A, B> = A extends B ? true : false 를 읽어보자.
A extends B는 다음과 같이 읽을 수 있다: A는 B의 부분 집합이다. 그렇기에 정리하면 A가 B의 부분집합 이라면 true 타입이고 그렇지 않다면 false다.
즉, A 타입의 모든 멤버가 B에 있어야 한다. B가 “제약이 존재하는” 인터페이스인 경우 A는 B의 제약 조건을 위반하지 않아야 한다.
임의의 자바스크립트를 나타낼 수 있는 두 가지 타입이 있다. 일반적인 것은 unknown으로 자바스크립트 값의 전체 집합이다.
// 1
type Y = string | number | boolean | object | bigint | symbol | null | undefined extends unknown ? 1 : 0;
// 1
type Y2 = {} | null | undefined extends unknown ? 1 : 0;
// 0
type N = unknown extends string ? 1 : 0;
조금 특이한 점이 있다:
unknown은 모든 원시 타입의 합집합이 아니다. Exclude<unknown, string>을 사용할 수 없다.unknown extends string | number | boolean | object | bigint | symbol | null | undefined는 false다.unknown이 더 큰 집합 이라는 것이다.대체로 unknown을 “가능한 모든 자바스크립트 값의 집합”으로 생각해도 된다. 하지만 any는 아니다.
any extends string ? 1 : 0 은 0 | 1 이다. 이는 자기도 모른다는 뜻이다.any extends never ? 1 : 0 은 0 | 1 이다. 이는 any가 공집합일수도 있다는 뜻이다.그러니 any를 어떤 집합인지 알 수 없는 집합이라고 생각해야한다. 추가 검사에서 string extends any, unknown extends any 및 any extends any가 모두 true임을 알 수 있는데, 이는 역설이다.
모든 집합은 any의 부분집합이지만 any는 공집합일 수 있기 때문이다.
그나마 다행인 점은 any extends unknown이 유효하다는 점이다. 즉, unknown은 전체집합으로 동작한다.

그래서 요약하자면 다음과 같다:
unknown은 전체 집합이다.never는 공집합이다.subtype = narrowed type = 교집합supertype = widened type = 초집합A extends B는 A는 B의 부분집합이다.& 및 |은 사실 집합의 합집합 및 교집합 연산이다.any는 역설이다.never 와 &(교집합)하면 never다. 공집합이기 때문이다.네, 검증된 내용을 바탕으로 다시 작성하겠습니다.
타입스크립트 타입 시스템은 서브 타이핑을 지원한다. 예를 들어, 타입 Cat이 Animal의 서브타입이라면, Cat 타입의 표현식은 Animal 타입의 표현식이 사용되는 모든 곳에서 대체 가능해야 한다.
가변성은 더 복잡한 타입들 간의 서브타이핑이 그들의 구성 요소 간의 서브타이핑과 어떻게 관련되는지를 나타낸다. 예를 들어:
Cat들의 리스트는 Animal들의 리스트와 어떤 관계여야 할까?Cat을 반환하는 함수는 Animal을 반환하는 함수와 어떤 관계여야 할까?타입 생성자의 가변성에 따라, 단순 타입들 간의 서프타이핑 관계는 해당하는 복잡한 타입들에 대해 다음과 같이 될 수 있다:
"같은 방향으로 변하는 특성"을 가진다. 타입 시스템에서 서브타입 관계가 복합 타입으로 확장될 때, 그 관계가 원래의 서브타입 방향과 동일하게 유지되는 특성을 의미한다.
interface Animal { name: string }
interface Dog extends Animal { bark(): void }
// 공변성 예시: 반환 타입
type AnimalGetter = () => Animal;
type DogGetter = () => Dog;
const getDog: DogGetter = () => ({
name: "뭉치",
bark: () => console.log("왈왈")
});
const getAnimal: AnimalGetter = getDog; // ✅ OK
타입스크립트에서 배열도 공변적이다:
const dogs: Dog[] = [
{ name: "뭉치", bark: () => console.log("왈왈") }
];
const animals: Animal[] = dogs; // ✅ OK
제네릭 타입도 공변적이다:
interface Box<T> {
value: T;
}
const dogBox: Box<Dog> = {
value: { name: "뭉치", bark: () => console.log("왈왈") }
};
const animalBox: Box<Animal> = dogBox; // ✅ OK
"반대 방향으로 변하는" 특성을 가진다. 타입 A가 타입 B의 서브타입일 때, F<B>가 F<A>의 서브타입이 되는 타입 관계의 특성을 의미한다:
interface Animal { name: string }
interface Dog extends Animal { bark(): void }
// 공변성 예시: 반환 타입
type AnimalGetter = () => Animal;
type DogGetter = () => Dog;
const getDog: DogGetter = () => ({
name: "뭉치",
bark: () => console.log("왈왈")
});
const getAnimal: AnimalGetter = getDog; // ✅ OK
// 반공변성 예시: 매개변수
type AnimalFn = (x: Animal) => void;
type DogFn = (x: Dog) => void;
const animalGreet: AnimalFn = (animal: Animal) => {
console.log(`Hello ${animal.name}`);
};
const dogGreet: DogFn = animalGreet; // ✅ OK
여기서 반공변성이 작동하는 이유는:
Dog는 Animal의 서브타입이다.
그러나 함수 타입에서는 이 관계가 역전된다.
AnimalFn을 DogFn에 할당할 수 있다. 즉, 더 일반적인 타입(Animal)을 매개변수로 받는 함수를 더 구체적인 타입(Dog)을 매개변수로 받는 함수 타입에 할당할 수 있다.
이것이 반공변성의 핵심 특성이다. 매개변수 타입 관계가 원래의 타입 관계와 반대 방향으로 흐르는 것을 보여준다.
함수의 매개변수 타입은 반공변적 위치에 있다. 이는 함수의 대체 가능성 원칙과 직접적으로 연관된다.
이정도의 개념만 이해해도 어렵게 작성된 타입 시스템을 이해하는데 정말 많은 도움을 받았다. 타입스크립트에 대해 공부한 내용은 조금 더 많았고 전체적인 내용은 링크에서 확인할 수 있다.
타입시스템이 어디에 근간을 두고 있는지, 타임 추론 시스템이 어떻게 동작하는지 AST의 구조를 보며 설명한 섹션 등이 있다. 조금 더 깊은 이해를 원하는 사람들을 위해 아래 출처에 내가 참고한 다양한 자료의 링크를 남겨두었다.
나는 infer, never만 보면 두려워지는 당신을 위한 타입 추론 - 응용 문제
이 글이 정말 이해하기가 어려웠었다.
최근에 이 글을 완벽히 이해하는데 성공했고 그 기반에는 집합 이론과, 타입시스템의 공변성과 반 공변성 등의 개념에 대한 이해가 있다. 만약 이런 개념에 대해 이해하고 타입시스템에 대한 기반이 쌓였다면 위 글을 읽어보길 추천한다.
Understanding Algorithm W: 타입시스템의 근간이 되는 W 알고리즘에 대한 자료
interactive type inference: 타입 추론이 어떻게 동작하는지 UI로 보여주는 웹사이트
Hindley–Milner type system: HM 타입 시스템에 대한 wikipedia 자료
To Type or Not to Type: Quantifying Detectable Bugs in JavaScript
Making sense of TypeScript using set theory
Flow Nodes: How Type Inference Is Implemented: 타입 추론이 어떻게 동작하는지에 대해 세부 구현과 함께 설명해주는 자료
이해하기 쉬운 이미지와 함께 있어 읽기 편했습니다. 기환님의 타입스크립트에 대한 꼼꼼한 고찰이 드러나네요. 멋집니다. 특히 Unknown과 Any 부분이 흥미로웠어요. js에서 처음으로 ts를 도입할 때는 개발 시간도 더 많이 들어 도입하는데에 있어 좋지만은 않았던 기억이 나네요. ( 지금은 없으면 개발을 못합니다ㅎㅎㅎㅋㅋ ) 38%의 버그를 타입스크립트 사용으로 예방할 수 있다니 .... 타입스크립트에 대해 더더욱 잘 알고 사용하고 싶어지는군요.
타입스크립트를 사용하면서 제일 좋은 점은 코드 작성 시 안전성과 효율성을 동시에 챙길 수 있다는 점 같아요.
좋은 글 덕분에 간과하고 넘어갔던 부분도 알게되었던거 같아요
타입스크립트의 필요성에 대해서 실제 연구결과 지표를 보여주시니 더욱 체감이되네요! ㅎㅎ
타입스크립트를 많이 사용해보지 않아서 잘 몰랐는데, 이 글을 보면서 다시 공부해야겠다는 생각이드네요! 좋은 글 감사합니다 :)
타입스크립트의 내부 동작 원리에 대해서 정리해주신글 잘 읽었습니다! 실제로 백엔드 언어들의 구조적 타이핑과도 다른점이 있을지도 궁금하네요, 저도 그동안 모호했던 타입 관계에 대해서 정리하는 시간을 조금 가져봐야겠습니다. 잘 읽고 갑니다 기환님! 다음 포스팅도 기대하겠습니다! 👏
내용이 아주 풍부하고 체계적인 타입스크립트 글이네요. 특히, 타입스크립트가 제공하는 안전성과 효율성, 그리고 내부 동작 원리까지 상세하게 설명된 점이 인상적입니다. 이런 글은 타입스크립트를 학습하거나 실제 프로젝트에서 더 잘 활용하고자 하는 개발자들에게 큰 도움이 될 것 같습니다.
저도 타입스크립트의 핵심 개념들(덕 타이핑, 명목적 타이핑, 구조적 타이핑)과 집합 이론을 바탕으로 한 타입 연산의 접근 방식이 인상 깊었어요. 특히 Unknown과 Any의 차이점이나 extends와 관련된 집합 이론의 연결점에 대한 설명이 흥미로웠습니다.
마지막으로, 타입스크립트의 가변성(공변성, 반공변성, 이변성, 불변성)에 대한 설명이 현실적인 예제를 통해 잘 전달된 것 같습니다. 이런 구조적이고 체계적인 글은 타입스크립트를 더 깊이 이해하려는 개발자들에게 큰 도움을 줄 것입니다.
기환님의 글을 읽고 나니 타입스크립트를 더 깊이 탐구해보고 싶어지네요! 😊
정성이 느껴지는 좋은 글이네요 최고 👍👍👍
정리가 너무 잘 되어 있어서 스크랩해두고 필요한 부분이 있을 때 마다 틈틈이 읽어도 좋을 것 같아요 잘 읽었습니다 감사합니다!!
타입의 도식화 및 unknown, any에 감탄하면서 글을 읽다가 원문을 대부분 번역한 글이라는 것을 나중에 알았네요 이 정도 내용의 번역 글이라면 번역 글이라고 크게 명시해야되지 않을까 싶네요 ..
아무튼, 내용 중
'이는 전혀 타입의 동작 방식이 아니다. Sum9 타입은 “9를 얻기 위해 적절한 sum에 액세스할 수 있는 것” 을 의미한다.'
이 부분은 단순 번역기 돌린 번역 자체는 틀린 말이 아닐 수 있으나 맥락상 해석을 포함하여 번역이 필요해 보입니다.
안녕하세요, 학습하신 내용 공유해주셔서 감사합니다. 정말 잘 읽었습니다.
다만 불변성에 대한 내용에 제가 알던 것과 다른 내용이 있어 질문드립니다.
interface Animal { name: string }
interface Dog extends Animal { bark(): void }
interface Box<T> {
value: T;
}
let dogBox: Box<Dog> = {
value: { name: "뭉치", bark: () => console.log("왈왈") }
};
let animalBox: Box<Animal> = dogBox;
Typescript Playground에 위 코드를 붙여넣었는데 타입 에러가 보이진 않네요.
제가 추가로 학습한 결과, 제네릭에 불변성이 강제되는 규칙은 Java에 해당하는 것으로, 타입스크립트 컴파일러는 함수 매개변수에 대한 부분을 제외하면 공변성을 따를 때 타입 호환을 허용하기 때문에 위 코드가 문제되지 않는 것 같다는 생각이 들었습니다.
잘못된 부분이 있다면 알려주시면 감사하겠습니다. 좋은 하루 되세요.
역시 기환님 bb 타입스크립트에 대해서 잘 작성해주셨군요bb 전체적으로 타입스크립트가 저희가 개발하면서 왜 중요한 역할을 하는지, 그리고 개발 프로세스에서 발생할 수 있는 여러 문제들을 잘 해결할 수 있는 다양한 사례를 들어주시면서 잘 설명해주셔서 너무 좋았습니다 저도 타입스크립트 쓰면서 몰랐던 부분도 같이 알아갔습니다 bb 넘 고생많으셨습니다 ~!