일.존재 의의
이러한 시나리오를 고려합니다. identity 함수는 1 개의 파라미터를 받아들이고, 그대로 반환합니다:
function identity(arg) {
return arg;
}
타입에서 보면, 파라미터가 어떠한 타입이어도, 반환값의 타입은 파라미터와 일치합니다. [오버로드 메커니즘](/articles/함수-typescript 노트 5/#articleHeader9) 을借助하여, 이렇게 기술할 수 있습니다:
function identity(arg: number): number;
function identity(arg: string): string;
function identity(arg: boolean): boolean;
// ...等无数个 a => a 의 타입 기술
오버로드는 이 시나리오를 만족할 수 없는 것 같습니다. arg 의 모든 가능한 타입을 열거할 방법이 없기 때문입니다. 파라미터가 임의의 타입이므로, any 를 시도해 봅시다:
function identity(arg: any): any;
모든 타입을 커버했지만, 파라미터와 반환값의 타입 대응 관계를 잃었습니다 (위는 A => B 의 타입 매핑에 상당하며, 우리가 기술하고 싶은 것은 A => A 입니다)
제네릭과 any
그렇다면, 2 개의 any 간의 대응 관계를 어떻게 표현하면 될까요?
제네릭을 사용합니다. 이렇게 기술:
function identity<T>(arg: T): T {
return arg;
}
타입 변수 T 는 any 에 유사하며, 具名 any 에 상당 합니다. 이렇게 하면 T => T (즉 A => A) 의 타입 매핑을 명확하게 표현할 수 있습니다
이.타입 변수
Type variable, a special kind of variable that works on types rather than values.
일반 변수는 값을 나타내고, 타입 변수는 타입을 나타냅니다
작용에서 보면, 변수는 값을 운반할 수 있고, 타입 변수는 타입 정보를 운반합니다:
This allows us to traffic that type information in one side of the function and out the other.
삼.제네릭 함수
타입 변수는 타입 파라미터라고도 불리며, 함수 파라미터와 유사합니다. 차이는 함수 파라미터는 구체적인 값을 받아들이고, 타입 파라미터는 구체적인 타입을 받아들이는 것입니다. 예를 들어:
function identity<T>(arg: T): T {
return arg;
}
// 传参给类型参数
// identity<number>
// 传参给函数参数(自动推断类型参数)
identity(1);
// 传参给函数参数(显式传入类型参数)
identity<number>(1);
타입 파라미터를 가진 함수를 제네릭 함수라고 부르며, 그 중에서 타입 파라미터는 임의의 타입 (any and all types) 을 나타냅니다. 따라서, 모든 타입에 공통적인 특징만 액세스할 수 있습니다:
function loggingIdentity<T>(arg: T): T {
// 报错 Property 'length' does not exist on type 'T'.
console.log(arg.length);
return arg;
}
실제, void 라는 공집합이 있기 때문에, 모든 타입에 공통적인 속성이나 메서드는 존재하지 않습니다. 타입 변수에 대해 가정을 할 수도 없습니다 (예를 들어 length 속성이 있다고 가정). 임의의 타입을 나타내며, 아무런 제약이 없기 때문입니다
除此之外, 타입 변수 T 는 구체적인 타입처럼, 구체적인 타입이 나타나는 모든 곳에서 사용할 수 있습니다:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
// 또는
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
타입 기술
제네릭 함수의 타입 기술은 일반 함수와 유사:
// 普通 함수
let myIdentity: (arg: string) => string =
function(arg: string): string {
return arg;
};
// 제네릭 함수
let myIdentity: <T>(arg: T) => T =
function<T>(arg: T): T {
return arg;
};
여전히 애로우 함수 구문이지만, (파라미터리스트) 앞에 <타입파라미터리스트> 를 추가했을 뿐입니다. 마찬가지로, 타입 기술 중의 타입 파라미터명도 실제와 일치하지 않아도 됩니다:
let myIdentity: <U>(arg: U) => U =
function<T>(arg: T): T {
return arg;
};
P.S. 特殊的, 함수 타입 기술은 오브젝트 리터럴 형식으로도 기술 가능:
// 제네릭 함수
let myIdentity: { <T>(arg: T): T };
// 普通 함수
let myIdentity: { (arg: string): string };
인터페이스 형식 타입 기술의 퇴화 버전과 같아, 재사용의 우위성이 없고, 애로우 함수만큼 간결하지도 않아, 따라서, 그다지 일반적이지 않습니다
사.제네릭 인터페이스
타입 파라미터를 가진 인터페이스를 제네릭 인터페이스라고 부르며, 예를 들어 인터페이스를 사용하여 제네릭 함수를 기술:
interface GenericIdentityFn {
<T>(arg: T): T;
}
매우 유사한 형식이 또 하나 있습니다:
interface GenericIdentityFn<T> {
(arg: T): T;
}
이들은 모두 제네릭 인터페이스라고 불립니다. 차이는 후자의 타입 파라미터 T 가 인터페이스 전체에 작용한다는 것입니다. 예를 들어:
interface GenericIdentity<T> {
id(arg: T): T;
idArray(...args: T[]): T[];
}
let id: GenericIdentity<string> = {
id: (s: string) => s,
// 报错 Types of parameters 's' and 'args' are incompatible.
idArray: (...s: number[]) => s,
};
인터페이스 레벨의 타입 파라미터에는 이러한 구속 작용이 있지만, 멤버 레벨의 것은 없습니다 (該 제네릭 멤버에만 작용)
오.제네릭 클래스
마찬가지로, 타입 파라미터를 가진 클래스를 제네릭 클래스라고 부르며, 예를 들어:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
인터페이스처럼, 제네릭 클래스는 該 클래스의 모든 멤버가 주목하는 목표 타입이 일치하는 것을 구속할 수 있습니다:
Putting the type parameter on the class itself lets us make sure all of the properties of the class are working with the same type.
주의, 타입 파라미터는 클래스 중의 인스턴스 멤버에만 적용되며, 정적 멤버는 타입 파라미터를 사용할 수 없습니다. 예를 들어:
class GenericNumber<T> {
// 报错 Static members cannot reference class type parameters.
static zeroValue: T;
}
정적 멤버는 클래스 인스턴스 간에 공유되며, 타입 파라미터의 구체적인 타입을 유일하게 확정할 수 없기 때문에:
let n1: GenericNumber<string>;
// 期望 n1.constructor.zeroValue 는 string
let n2: GenericNumber<number>;
// 期望 n1.constructor.zeroValue 는 number, 모순이 발생
P.S. 이 점은 Java 와 일치, 상세는 Static method in a generic class? 참조
육.제네릭 구속
타입 파라미터는 너무 "汎"해서 (any and all), 몇 가지 시나리오에서는, 구속을 가하고 싶을 수 있습니다. 예를 들어:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
인터페이스를 통해 타입 파라미터에 대한 구속을 기술 (T extends constraintInterface), 예를 들어 위는 타입 파라미터 T 가 number 타입의 length 속성을 가져야 한다고 요구
또 하나의 전형적인 시나리오는 팩토리 메서드, 예를 들어:
// 要求构造函数 c 는 반드시 동일 클래스 (또는 서브클래스) 의 인스턴스를 반환
function create<T>(c: {new(): T; }): T {
return new c();
}
此外, 제네릭 구속 중에서 타입 파라미터를 사용하는 것도 가능, 예를 들어:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
1 개의 타입 파라미터의 특징을 사용하여 다른 타입 파라미터를 구속할 수 있어, 매우 강력합니다
칠.정리
제네릭이라고 불리는 이유는, 일련의 타입에 작용할 수 있기 때문이며, 구체적인 타입 위의 한 층의 추상입니다:
Generics are able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.
참고 자료
-
[타입 파라미터 | 타입_Haskell 노트 3](/articles/타입-haskell 노트 3/#articleHeader9)
아직 댓글이 없습니다