[Typescript]타입 호환성과 Freshness

Choise.o·2024년 6월 8일
0

1. 타입시스템이란?

: 프로그래밍 언어에서 변수가 어떤 종류의 값(데이터)를 가질 수 있는지 정의하고 검증하는 규칙

2. 타입 시스템의 종류

  • 명목적 타입 시스템 : 두 타입의 이름이 같아야 같은 타입으로 간주한다.
    ex) java는 클래스명이 동일해야 같은 타입으로 인식됨

  • 구조적 타입 시스템 : 두 타입의 구조가 같으면 같은 타입으로 간주한다.
    ⇒ 객체의 속성과 메서드가 동일하면 같은 타입으로 인식됨

  • typescript는? 구조적 타입시스템을 사용한다

    • javascript는 익명함수나 객체 리터럴을 많이 사용하기 때문
    • typescript는 javascript의 슈퍼셋인 오픈소스 프로그래밍 언어이다
// 익명함수
function() {
	return "anonymous function";
}

// 객체 리터럴
const obj_literal = {
	name: 'jane',
	age: 25 
}

3. typescript의 타입 호환성

: typescript의 타입 호환성은 구조적 서브타이핑(strunctural subtyping)을 기반으로 한다.

  • 구조적 타이핑? ⇒ 오직 멤버만으로 타입을 관계시키는 방식. 덕 타이핑(duck typing)이라고도 함
type Food = {
    protein: number;
    carbon: number;
    fat: number;
}

// parameter 타입을 Food로 지정해준다
function calculateCalories(food: Food) {
    return food.protein * 4 + food.carbon * 4 + food.fat * 9;
}

const food1:Food = {
    protein: 10,
    carbon: 10,
    fat: 10
}

calculateCalories(food1); // 170


/**
* 구조적 서브타이핑 : 상속관계 명시X. 객체의 프로퍼티 기반으로 타입 호환
*/
const food2 = {
    protein: 15,
    carbon: 15,
    fat: 15
}
calculateCalories(food2); // 255


/**
* 명시적 서브타이핑 : 상속관계 명시O 
*/
// Food 타입을 상속받은 Burger 타입을 정의하고
type Burger = Food & {
    brandName: string;
}

const burger1:Burger = {
    protein: 20,
    carbon: 20,
    fat: 20,
    brandName: 'mc'
}

// 함수의 인자로 전달해도 문제가 발생하지 않는다 => 타입호환성 때문
calculateCalories(burger1); // 340

4. 타입호환 예외 조건: Freshness

Fresh literal은 타입이 호환되지 않는다.

// 함수 인자로 literal 객체를 직접 전달하면 ?? => 에러 발생
calculateCalories({
    protein: 30,
    carbon: 30,
    fat: 30,
    brandName: 'burger king'
}); 
// ERROR : Object literal may only specify known properties, 
// 		and 'brandName' does not exist in type 'Food'.

Fresh literal 를 알아보기 전에..

typescript는 어떻게 타입을 검사할까?

typescript 컴파일러의 동작 과정을 간단히 정리해보면,

1) 소스코드를 AST(Abstract Syntax Tree)로 변환한 뒤 (parser.ts, scanner.ts)
2) 타입 검사를 수행하고 (binder.js, checker.ts)
3) javascript 소스코드로 변환한다. (emitter.js, transformer.ts)

Markdown 로고 >> typescript compiler Github

이때 구조적 서브타이핑타입 호환에 관한 부분은 타입 검사와 연관되어있는
checker.js의 hasExcessProperties() 함수에서 처리한다

// checker.js

// 1) getObjectFlags로 객체 타입 플래그 체크 
//    -> ObjectFlags.FreshLiteral 일 경우
const isPerformingExcessPropertyChecks = 
    getObjectFlags(source) & ObjectFlags.FreshLiteral;

// 2) FreshLiteral에 대한 excess properties 존재 여부를 체크한다. 
if (isPerformingExcessPropertyChecks) {
  
    if (hasExcessProperties(source as FreshObjectLiteralType)) {
        // excess properties가 존재할 경우 error
        reportError();
    }
  
}

// FreshLiteral이 아닌 경우 위 if 분기를 skip. 타입 호환을 허용
.
.
.

Fresh Literal 이란 무엇일까

모든 object literal 은 초기에 fresh하다고 간주되며

1) 타입 단언(type assertion)을 하거나,
2) 타입 추론에 의해 object literal의 타입이 확장되면 freshness가 사라진다.

⇒ object literal을 변수에 할당하는 경우, 이 두 가지 중 하나가 발생되어 freshness가 사라진다.

위에서는 함수에 object literal을 인자로 바로 전달했기 때문에 freshness가 사라지지 않았고, 타입이 호환되지 않아 오류가 발생했다.


그렇다면 왜 Fresh literal은 타입 호환을 허용하지 않을까?

⇒ 코드 파악을 어렵게 하고, 오류를 숨기는 등 여러 부작용을 유발할 수 있다.


앞서 사용한 calculateCalories 함수를 예시로 들자면,
  • 다른 개발자가 excess properties를 함수 연산에 필수적인 값으로 오해할 수 있다.
    ( calculateCalories에서는 excess properties로 전달한 brandName이 사용되지 않는데 필수값으로 오해할 수 있음 )

  • excess properties 이름에 오타가 있더라고 발견되지 않는다.
    ( type Burger 인 경우에만 호환을 허용하고 싶은데 Burger 에 없는 brandNeam 프로퍼티를 전달한 경우에도 허용한다면 문제가 된다 )


// 부작용 1
const calorie1 = calculateCalories({
  protein: 29,
  carbon: 48,
  fat: 13,
  burgerBrand: 'burger king' // ???? calculateCalorie에 필요한지 알 수 없음
});

// 부작용 2
const calorie2 = calculateCalories({
  protein: 29,
  carbon: 48,
  fat: 13,
  brandNeam: 'wrong name' // ???? property 이름에 오타가 있는지 확인할 수 없음
});

Fresh object에 대해서는 타입 호환성을 제공해서 얻는 이점보다는 부작용이 더 많아
타입 호환성을 지원하지 않는다.


그럼에도 fresh object에 대한 타입 호환성을 허용하고자 한다면
index signature를 사용하거나 tsconfig 에 suppressExcessPropertyErrors=true로 설정하면 된다.


// index signature 를 사용하는 방법
type Food = {
    protein: number;
    carbon: number;
    fat: number;
    [key: string]: any;	// excess property 를 추가할 수 있게 한다
}

반대로 타입 호환성을 엄격하게 제한하고 싶다면 브랜드 타입을 사용하면 된다.

type Brand<K, T> = K & {__brand: T};

type Food = Brand<{
    protein: number;
    carbon: number;
    fat: number;
}, 'Food'>

const burger1 = {
    protein: 20,
    carbon: 20,
    fat: 20,
    brandName: 'mc'
}

calculateCalories(burger1); // ERROR : Argument of type 
					// '{ protein: number; carbon: number; fat: number; brandName: string; }' 
					// is not assignable to parameter of type 'Food'.

5. 출처

0개의 댓글