제네릭
제네릭 (Generics)
재사용 가능한 컴포넌트를 만들 때, "오늘의 데이터뿐 아니라 내일의 데이터에도 동작하는" 컴포넌트가 가장 유연하죠. 제네릭은 C#과 Java 같은 언어에서 재사용 가능한 컴포넌트를 만드는 핵심 도구 중 하나입니다. 단일 타입이 아니라 다양한 타입에 걸쳐 동작할 수 있는 컴포넌트를 만들 수 있게 해 주죠.
출처: TypeScript 핸드북
소프트웨어 엔지니어링의 큰 부분은 잘 정의되고 일관된 API를 가질 뿐 아니라 재사용 가능한 컴포넌트를 만드는 것입니다. 오늘의 데이터뿐 아니라 내일의 데이터에도 동작할 수 있는 컴포넌트가 큰 소프트웨어 시스템을 구축하는 데 가장 유연한 능력을 줄 거예요.
C#과 Java 같은 언어에서 재사용 가능한 컴포넌트를 만드는 주요 도구 중 하나는 제네릭, 즉 단일 타입이 아니라 다양한 타입에 걸쳐 동작할 수 있는 컴포넌트를 만드는 것입니다. 이렇게 하면 사용자가 해당 컴포넌트를 가져다 자신의 타입을 사용할 수 있죠.
제네릭의 Hello World
시작으로 제네릭의 "hello world", 즉 항등 함수를 해 봅시다. 항등 함수는 무엇이 들어오든 그대로 반환하는 함수입니다. echo 명령과 비슷하게 생각할 수 있어요.
제네릭이 없으면 항등 함수에 특정 타입을 부여해야 합니다.
function identity(arg: number): number {
return arg;
}
또는 any 타입으로 항등 함수를 설명할 수 있어요.
function identity(arg: any): any {
return arg;
}
any를 사용하면 arg의 타입으로 모든 타입을 받도록 함수가 제네릭이 된다는 점에서 확실히 제네릭이지만, 함수가 반환할 때 그 타입이 무엇이었는지에 대한 정보를 실제로는 잃어버립니다. 숫자를 전달했다면, 우리가 가진 유일한 정보는 어떤 타입이든 반환될 수 있다는 것입니다.
대신 우리는 인자의 타입을 포착해서 반환되는 것이 무엇인지를 표시하는 데도 그 타입을 사용할 수 있는 방법이 필요해요. 여기서 우리는 값이 아니라 타입에 대해 작동하는 특별한 종류의 변수인 타입 변수 를 사용할 겁니다.
function identity<Type>(arg: Type): Type {
return arg;
}
이제 항등 함수에 타입 변수 Type을 추가했습니다. 이 Type은 사용자가 제공하는 타입(예: number)을 포착해서 나중에 그 정보를 사용할 수 있게 해 줍니다. 여기서 우리는 Type을 반환 타입으로 다시 사용합니다. 살펴보면 이제 인자와 반환 타입에 같은 타입이 사용되는 것을 볼 수 있어요. 이로써 그 타입 정보를 함수의 한쪽에서 들어오고 다른 쪽으로 빠져나가게 운반할 수 있습니다.
이 버전의 identity 함수는 다양한 타입에 걸쳐 동작하므로 제네릭이라고 말합니다. any를 사용한 것과 달리, 인자와 반환 타입에 숫자를 사용했던 첫 번째 identity 함수만큼이나 정확합니다(즉 정보를 전혀 잃지 않아요).
제네릭 항등 함수를 작성했다면 두 가지 방법 중 하나로 호출할 수 있어요. 첫 번째 방법은 타입 인자를 포함해 모든 인자를 함수에 전달하는 것입니다.
function identity<Type>(arg: Type): Type {
return arg;
}
// ---cut---
let output = identity<string>("myString");
// ^?
여기서 우리는 함수 호출의 인자 중 하나로 Type을 string으로 명시적으로 설정했으며, 인자 주변에 ()가 아니라 <>를 사용해 나타냈습니다.
두 번째 방법은 아마 가장 흔할 거예요. 여기서는 타입 인자 추론 을 사용합니다 — 즉 전달하는 인자의 타입을 기반으로 컴파일러가 Type 값을 자동으로 설정하길 원하는 것이죠.
function identity<Type>(arg: Type): Type {
return arg;
}
// ---cut---
let output = identity("myString");
// ^?
각괄호(<>) 안에 타입을 명시적으로 전달할 필요가 없었다는 점을 주목하세요. 컴파일러는 그저 값 "myString"을 보고 Type을 그 타입으로 설정했습니다. 타입 인자 추론은 코드를 더 짧고 읽기 쉽게 유지하는 데 유용한 도구가 될 수 있지만, 더 복잡한 예시에서처럼 컴파일러가 타입을 추론하지 못할 때는 이전 예시에서처럼 타입 인자를 명시적으로 전달해야 할 수도 있어요.
제네릭 타입 변수 다루기
제네릭을 사용하기 시작하면, identity 같은 제네릭 함수를 만들 때 컴파일러가 함수 본문에서 제네릭 타입이 지정된 매개변수를 올바르게 사용하도록 강제한다는 것을 알게 될 거예요. 즉, 그러한 매개변수를 어떤 타입이든 될 수 있는 것처럼 실제로 취급해야 한다는 뜻이죠.
이전의 identity 함수를 다시 가져와 봅시다.
function identity<Type>(arg: Type): Type {
return arg;
}
매 호출마다 인자 arg의 길이도 콘솔에 기록하고 싶다면 어떨까요? 아마 다음과 같이 쓰고 싶을 거예요.
// @errors: 2339
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
이렇게 하면 컴파일러가 arg의 .length 멤버를 사용하고 있다는 에러를 주는데, 우리는 어디에서도 arg가 이 멤버를 갖고 있다고 말하지 않았습니다. 앞서 말했듯이 이 타입 변수들은 어떤 타입과 모든 타입을 대신하기 때문에, 이 함수를 사용하는 사람이 .length 멤버가 없는 number를 전달했을 수도 있어요.
사실 우리가 의도한 것은 이 함수가 Type 자체가 아니라 Type의 배열에 대해 동작하는 것이었다고 해 봅시다. 배열을 다루고 있으므로 .length 멤버는 사용 가능해야 해요. 다른 타입의 배열을 만들듯이 이를 설명할 수 있습니다.
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
loggingIdentity의 타입은 "제네릭 함수 loggingIdentity는 타입 매개변수 Type과 Type들의 배열인 인자 arg를 받고, Type들의 배열을 반환한다"로 읽을 수 있어요. 숫자 배열을 전달하면 Type이 number에 바인딩되므로 숫자 배열을 돌려받게 됩니다. 이렇게 하면 제네릭 타입 변수 Type을 전체 타입이 아니라 작업 중인 타입의 일부로 사용할 수 있어 더 큰 유연성을 얻을 수 있어요.
같은 예시를 이렇게도 쓸 수 있습니다.
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
다른 언어에서 이 스타일의 타입을 이미 알고 있을 수도 있어요. 다음 섹션에서는 Array<Type> 같은 제네릭 타입을 직접 만드는 방법을 다룰 거예요.
제네릭 타입 (Generic Types)
이전 섹션에서 우리는 다양한 타입에 걸쳐 동작하는 제네릭 항등 함수를 만들었습니다. 이 섹션에서는 함수 자체의 타입과 제네릭 인터페이스를 만드는 방법을 탐구해 볼 거예요.
제네릭 함수의 타입은 함수 선언처럼 타입 매개변수를 먼저 나열한다는 점만 빼면 비제네릭 함수의 타입과 동일합니다.
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;
타입 변수의 개수와 사용 방식이 맞다면 타입에서 제네릭 타입 매개변수에 다른 이름을 사용할 수도 있어요.
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Input>(arg: Input) => Input = identity;
제네릭 타입을 객체 리터럴 타입의 호출 시그니처로도 작성할 수 있습니다.
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;
이것이 우리의 첫 번째 제네릭 인터페이스를 작성하게 이끕니다. 이전 예시의 객체 리터럴을 가져와 인터페이스로 옮겨 봅시다.
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
비슷한 예시에서, 제네릭 매개변수를 인터페이스 전체의 매개변수로 옮기고 싶을 수도 있어요. 그러면 우리가 무엇에 대해 제네릭인지(예: Dictionary가 아니라 Dictionary<string>) 볼 수 있게 됩니다. 이렇게 하면 타입 매개변수가 인터페이스의 다른 모든 멤버에게 보이게 돼요.
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
우리의 예시가 약간 다른 것으로 바뀌었다는 점을 주목하세요. 제네릭 함수를 설명하는 대신, 이제 우리는 제네릭 타입의 일부인 비제네릭 함수 시그니처를 갖게 되었습니다. GenericIdentityFn을 사용할 때는 이제 해당 타입 인자(여기서는 number)도 지정해야 하며, 이는 근본적인 호출 시그니처가 사용할 것을 사실상 고정합니다. 타입 매개변수를 호출 시그니처에 직접 둘지, 인터페이스 자체에 둘지 이해하는 것은 타입의 어떤 측면이 제네릭인지 설명하는 데 도움이 됩니다.
제네릭 인터페이스 외에도 제네릭 클래스를 만들 수 있어요. 제네릭 enum과 namespace는 만들 수 없다는 점에 주의하세요.
제네릭 클래스 (Generic Classes)
제네릭 클래스는 제네릭 인터페이스와 비슷한 모양을 갖습니다. 제네릭 클래스는 클래스 이름 뒤에 각괄호(<>) 안에 제네릭 타입 매개변수 목록을 가집니다.
// @strict: false
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
이것은 GenericNumber 클래스의 꽤나 문자 그대로의 사용이지만, number 타입만 사용하도록 제한하는 것이 없다는 것을 알아챘을 거예요. 대신 string이나 더 복잡한 객체를 사용했을 수도 있어요.
// @strict: false
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
// ---cut---
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
인터페이스와 마찬가지로, 타입 매개변수를 클래스 자체에 두면 클래스의 모든 프로퍼티가 같은 타입으로 동작하도록 보장할 수 있어요.
클래스 섹션에서 다루듯이, 클래스의 타입에는 정적(static) 측면과 인스턴스(instance) 측면이라는 두 측면이 있습니다. 제네릭 클래스는 정적 측면이 아니라 인스턴스 측면에 대해서만 제네릭이므로, 클래스를 다룰 때 정적 멤버는 클래스의 타입 매개변수를 사용할 수 없습니다.
제네릭 제약 (Generic Constraints)
이전 예시에서 기억한다면, 그 타입들의 집합이 어떤 능력을 가질지에 대한 어느 정도의 지식을 가진 타입 집합에 대해 동작하는 제네릭 함수를 작성하고 싶을 때가 있습니다. loggingIdentity 예시에서 우리는 arg의 .length 프로퍼티에 접근하고 싶었지만, 컴파일러는 모든 타입이 .length 프로퍼티를 가진다는 것을 증명할 수 없어서 이 가정을 할 수 없다고 경고했습니다.
// @errors: 2339
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
아무 타입이나 다루는 대신, 이 함수를 .length 프로퍼티를 또한 가진 모든 타입들로 제약하고 싶습니다. 타입이 이 멤버를 갖는 한 허용되지만, 적어도 이 멤버는 가져야 합니다. 그러려면 우리의 요구사항을 Type이 무엇일 수 있는지에 대한 제약으로 나열해야 합니다.
그렇게 하려면 우리의 제약을 설명하는 인터페이스를 만들겠습니다. 여기서는 단일 .length 프로퍼티를 가진 인터페이스를 만든 다음, 이 인터페이스와 extends 키워드를 사용해 우리의 제약을 나타낼 겁니다.
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
이제 제네릭 함수가 제약되었으므로 더 이상 모든 타입에 대해 동작하지 않습니다.
// @errors: 2345
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length);
return arg;
}
// ---cut---
loggingIdentity(3);
대신, 타입이 그 모든 필수 프로퍼티를 가진 값들을 전달해야 합니다.
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length);
return arg;
}
// ---cut---
loggingIdentity({ length: 10, value: 3 });
제네릭 제약에서 타입 매개변수 사용하기
다른 타입 매개변수에 의해 제약된 타입 매개변수를 선언할 수 있어요. 예를 들어 여기서는 객체 이름이 주어졌을 때 객체에서 프로퍼티를 가져오고 싶습니다. obj에 존재하지 않는 프로퍼티를 실수로 가져오지 않도록, 두 타입 사이에 제약을 두고 싶어요.
// @errors: 2345
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
제네릭에서 클래스 타입 사용하기
TypeScript에서 제네릭으로 팩토리를 만들 때, 클래스 타입을 생성자 함수로 참조해야 합니다. 예를 들어,
function create<Type>(c: { new (): Type }): Type {
return new c();
}
더 고급 예시는 prototype 프로퍼티를 사용해 생성자 함수와 클래스 타입의 인스턴스 측면 사이의 관계를 추론하고 제약합니다.
// @strict: false
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
numLegs = 6;
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
이 패턴은 mixins 디자인 패턴을 구동하는 데 사용됩니다.
제네릭 매개변수 기본값 (Generic Parameter Defaults)
제네릭 타입 매개변수에 기본값을 선언함으로써, 해당 타입 인자를 지정하는 것이 선택 사항이 되게 만들 수 있습니다. 예를 들어 새 HTMLElement를 만드는 함수를 봅시다. 인자 없이 호출하면 HTMLDivElement를 만들고, 첫 번째 인자로 요소를 줘서 호출하면 그 인자 타입의 요소를 만듭니다. 선택적으로 children 목록을 전달할 수도 있어요. 예전에는 이 함수를 다음과 같이 정의해야 했습니다.
type Container<T, U> = {
element: T;
children: U;
};
// ---cut---
declare function create(): Container<HTMLDivElement, HTMLDivElement[]>;
declare function create<T extends HTMLElement>(element: T): Container<T, T[]>;
declare function create<T extends HTMLElement, U extends HTMLElement>(
element: T,
children: U[]
): Container<T, U[]>;
제네릭 매개변수 기본값을 사용하면 다음과 같이 줄일 수 있습니다.
type Container<T, U> = {
element: T;
children: U;
};
// ---cut---
declare function create<T extends HTMLElement = HTMLDivElement, U extends HTMLElement[] = T[]>(
element?: T,
children?: U
): Container<T, U>;
const div = create();
// ^?
const p = create(new HTMLParagraphElement());
// ^?
제네릭 매개변수 기본값은 다음 규칙을 따릅니다.
- 기본값이 있으면 타입 매개변수는 선택 사항으로 간주됩니다.
- 필수 타입 매개변수는 선택적 타입 매개변수 뒤에 올 수 없습니다.
- 타입 매개변수의 기본 타입은 그 타입 매개변수에 대한 제약이 있다면 그 제약을 만족해야 합니다.
- 타입 인자를 지정할 때는 필수 타입 매개변수에 대해서만 타입 인자를 지정하면 됩니다. 지정되지 않은 타입 매개변수는 기본 타입으로 해석됩니다.
- 기본 타입이 지정되었는데 추론이 후보를 선택할 수 없으면, 기본 타입이 추론됩니다.
- 기존 클래스 또는 인터페이스 선언과 병합되는 클래스 또는 인터페이스 선언은 기존 타입 매개변수에 대한 기본값을 도입할 수 있습니다.
- 기존 클래스 또는 인터페이스 선언과 병합되는 클래스 또는 인터페이스 선언은 기본값을 지정하는 한 새 타입 매개변수를 도입할 수 있습니다.
분산 어노테이션 (Variance Annotations)
이는 매우 특정한 문제를 해결하기 위한 고급 기능으로, 사용할 이유를 파악한 상황에서만 사용해야 합니다.
공변성과 반공변성은 두 제네릭 타입 사이의 관계가 무엇인지 설명하는 타입 이론 용어입니다. 이 개념에 대한 간단한 입문을 살펴보겠습니다.
예를 들어 특정 타입을 make할 수 있는 객체를 나타내는 인터페이스가 있다면,
interface Producer<T> {
make(): T;
}
Animal이 기대되는 곳에 Producer<Cat>을 사용할 수 있는데, Cat은 Animal이기 때문입니다. 이 관계를 공변성(covariance) 이라고 합니다: Producer<T>에서 Producer<U>로의 관계는 T에서 U로의 관계와 같습니다.
반대로 특정 타입을 consume할 수 있는 인터페이스가 있다면,
interface Consumer<T> {
consume: (arg: T) => void;
}
Cat이 기대되는 곳에 Consumer<Animal>을 사용할 수 있는데, Animal을 받아들일 수 있는 함수라면 반드시 Cat도 받아들일 수 있어야 하기 때문입니다. 이 관계를 반공변성(contravariance) 이라고 합니다: Consumer<T>에서 Consumer<U>로의 관계는 U에서 T로의 관계와 같습니다. 공변성과 비교해 방향이 뒤집히는 것에 주목하세요! 이것이 반공변성이 "자기 자신을 상쇄"하고 공변성은 그렇지 않은 이유입니다.
TypeScript와 같은 구조적 타입 시스템에서 공변성과 반공변성은 타입의 정의로부터 자연스럽게 따라 나오는 동작입니다. 제네릭이 없더라도 공변(및 반공변) 관계를 볼 수 있습니다:
interface AnimalProducer {
make(): Animal;
}
// A CatProducer can be used anywhere an
// Animal producer is expected
interface CatProducer {
make(): Cat;
}
TypeScript는 구조적 타입 시스템을 갖고 있으므로, 두 타입을 비교할 때(예를 들어 Producer<Animal>이 기대되는 곳에 Producer<Cat>을 사용할 수 있는지) 일반적인 알고리즘은 두 정의를 구조적으로 확장해 그 구조를 비교합니다. 그러나 분산성은 매우 유용한 최적화를 가능하게 합니다: Producer<T>가 T에 대해 공변적이라면, Producer<Cat>과 Producer<Animal>이 같은 관계를 가질 것임을 알기 때문에 Cat과 Animal만 확인하면 됩니다.
이 논리는 같은 타입의 두 인스턴스화를 검사할 때만 사용할 수 있다는 점에 주의하세요. Producer<T>와 FastProducer<U>가 있다면 T와 U가 반드시 이 타입들의 같은 위치를 가리킨다는 보장이 없으므로, 이 검사는 항상 구조적으로 수행됩니다.
분산성은 구조적 타입의 자연스럽게 발생하는 성질이기 때문에 TypeScript는 자동으로 모든 제네릭 타입의 분산성을 추론 합니다. 극히 드물게 특정 종류의 순환 타입을 포함하는 경우에 이 측정이 부정확할 수 있습니다. 이런 일이 발생하면 타입 매개변수에 분산 어노테이션을 추가해 특정 분산성을 강제할 수 있습니다:
// Contravariant annotation
interface Consumer<in T> {
consume: (arg: T) => void;
}
// Covariant annotation
interface Producer<out T> {
make(): T;
}
// Invariant annotation
interface ProducerConsumer<in out T> {
consume: (arg: T) => void;
make(): T;
}
구조적으로 발생해야 할 같은 분산성을 작성하는 경우에만 이렇게 하세요.
구조적 분산성과 일치하지 않는 분산 어노테이션은 절대 작성하지 마세요!
분산 어노테이션은 인스턴스화 기반 비교 중에만 효력이 있다는 점을 강조하는 것이 중요합니다. 구조적 비교 중에는 아무 효과가 없습니다. 예를 들어 분산 어노테이션으로 타입을 실제로 불변(invariant)으로 "강제"할 수는 없습니다:
// DON'T DO THIS - variance annotation
// does not match structural behavior
interface Producer<in out T> {
make(): T;
}
// Not a type error -- this is a structural
// comparison, so variance annotations are
// not in effect
const p: Producer<string | number> = {
make(): number {
return 42;
}
}
여기서 객체 리터럴의 make 함수는 number를 반환하는데, number가 string | number가 아니므로 에러를 일으킬 것이라고 기대할 수도 있습니다. 하지만 이것은 인스턴스화 기반 비교가 아닙니다. 객체 리터럴은 익명 타입이지 Producer<string | number>가 아니기 때문이죠.
분산 어노테이션은 구조적 동작을 바꾸지 않으며 특정 상황에서만 참고됩니다.
분산 어노테이션은 왜 하는지, 한계가 무엇인지, 언제 효력이 없는지 확실히 알 때만 작성하는 것이 매우 중요합니다. TypeScript가 인스턴스화 기반 비교를 사용할지 구조적 비교를 사용할지는 지정된 동작이 아니며, 정확성이나 성능상의 이유로 버전마다 바뀔 수 있습니다. 따라서 분산 어노테이션은 타입의 구조적 동작과 일치할 때만 작성해야 합니다. 특정 분산성을 "강제"하려고 분산 어노테이션을 사용하지 마세요. 코드에서 예측할 수 없는 동작을 일으킬 것입니다.
구조적 동작과 일치하지 않는 한 분산 어노테이션을 작성하지 마세요.
기억하세요, TypeScript는 제네릭 타입으로부터 분산성을 자동으로 추론할 수 있습니다. 분산 어노테이션을 작성할 필요는 거의 없으며, 특정한 필요를 파악했을 때만 작성해야 합니다. 분산 어노테이션은 타입의 구조적 동작을 바꾸지 않으며, 상황에 따라 인스턴스화 기반 비교를 기대했는데 구조적 비교가 이루어지는 것을 볼 수도 있습니다. 분산 어노테이션은 이러한 구조적 맥락에서 타입이 동작하는 방식을 수정하는 데 사용될 수 없으며, 어노테이션이 구조적 정의와 같지 않으면 작성해서는 안 됩니다. 이것은 올바르게 하기 어렵고 TypeScript는 대부분의 경우 분산성을 정확히 추론할 수 있기 때문에, 일반 코드에서 분산 어노테이션을 작성하게 되는 일은 없어야 합니다.
분산 어노테이션으로 typechecking 동작을 바꾸려 하지 마세요. 그것은 분산 어노테이션의 용도가 아닙니다.
"타입 디버깅" 상황에서는 임시 분산 어노테이션이 유용할 수 있습니다. 분산 어노테이션은 검사되기 때문이죠. 어노테이션된 분산성이 명백히 잘못되면 TypeScript가 에러를 발생시킵니다:
// Error, this interface is definitely contravariant on T
interface Foo<out T> {
consume: (arg: T) => void;
}
하지만 분산 어노테이션은 더 엄격할 수는 있습니다(예: 실제 분산성이 공변적이라면 in out은 유효합니다). 디버깅이 끝나면 분산 어노테이션을 반드시 제거하세요.
마지막으로, typechecking 성능을 극대화하고자 하고, 프로파일러도 실행했고, 느린 특정 타입을 파악했고, 분산성 추론이 특히 느리다고 파악했고, 작성하려는 분산 어노테이션을 신중히 검증했다면, 분산 어노테이션을 추가함으로써 아주 복잡한 타입에서 약간의 성능 이점을 볼 수 있습니다.
분산 어노테이션으로 typechecking 동작을 바꾸려 하지 마세요. 그것은 분산 어노테이션의 용도가 아닙니다.