TypeScript, 제네릭에서 클래스 타입 사용 (Using Class Types in Generics)

제네릭의 타입 어노테이션에 클래스 타입을 받기 위해서는 클래스의 생성자 함수 타입을 타입 어노테이션으로 받으면 됩니다. 즉, 다음처럼요.

function create<Type>(c: { new (): Type }): Type {
  return new c();
}

위의 함수는 클래스 타입을 제네릭 타입으로 받아 해당 클래스의 객체를 생성해줍니다. 예제 코드는 다음과 같습니다.

class Animal {
  constructor(name: string) { 
    // ...
  }
}
 
function createInstance<A>(c: new (name: string) => A, name: string): A {
  return new c(name);
}
 
createInstance(Animal, "tiger");

Animal의 생성자가 string 타입의 인자를 받는다는 점에 고려하여 클래스의 생성자 함수 타입을 정의하고 있고 있습니다.

TypeScript, 제네릭 제약조건에서 타입 매개변수 사용 (Using Type Parameters in Generic Constraints)

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}

위의 함수는 Type에 대한 객체를 첫번째 인자로 받고 두번째 인자로 Type에 대한 프로퍼티의 키를 받습니다. 예들들면 다음과 같습니다.

let x = { a: 1, b: 2, c: 3, d: 4 };
 
getProperty(x, "a");
getProperty(x, "m");

위의 코드에서 4번째 코드는 “m” 형식의 인수는 “a” | “b” | “c” | “d” 형식의 매개 변수에 할당될 수 없습니다.”라는 에러가 발생합니다.

참고로 상기 코드에서 keyof가 사용되었는데, keyof 연산자는 객체 타입에서 객체의 키 값들을 숫자나 문자열 리터럴 유니언을 생성합니다. 아래 타입 P는 "x" | "y"와 동일한 타입입니다.

type Point = { x: number; y: number };
type P = keyof Point;

TypeScript, 제네릭(Generic) 함수에 대한 타입

다음과 같은 제니릭 함수가 있습니다.

function identity<T>(arg: T): T {
  return arg;
}

상기 함수에 대한 타입은 어떻게 정의할 수 있을까.. 3가지 정도가 가능합니다. 먼저…

let myIdentity1: <T>(arg: T) => T = identity;

다음은 객체 리터럴 타입의 함수 호출 시그니처 방식입니다.

let myIdentity2: { <T>(arg: T): T } = identity;

다음은 인터페이스 방식입니다.

interface GenericIdentityFn1 {
  <T>(arg: T): T;
}
let myIdentity3: GenericIdentityFn1 = identity;

interface GenericIdentityFn2<T> {
  (arg: T): T;
}
let myIdentity4: GenericIdentityFn2<string> = identity;

TypeScript, 초과 속성 검사(Excess Property Checks) / 일반적인 타입 호환성(구조적 타이핑, structural typing) 검사 / 약한 타입 감지(Weak Type Detection)

다음과 같은 코드가 있다고 하겠습니다.

interface SquareConfig {
  color?: string;
  width?: number;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: config.color || "red",
    area: config.width ? config.width * config.width : 20,
  };
}

위의 코드를 사용하는 다음 코드는 에러입니다.

let mySquare = createSquare({ colour: "red", width: 100 });

인정할만 합니다. createSquare의 인자는 SquareConfig 타입으로, 이 타입은 color 속성을 갖고 있지 colour 속성이 아니기 때문입니다. 이를 초과 속성 검사(Excess Property Checks)라고 합니다. 그런데 다음 코드는 …

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

에러가 아닙니다. 앞서 에러가 있던 코드는 createSquare의 인자에 바로 객체 리터널를 직접 전달하고 있지만 두번째 에러가 없는 코드에서는 일단 객체 리터럴을 먼저 변수에 할당하고 그 변수를 함수에 전달하고 있습니다. 이렇게 하면 함수의 인자 타입과 변수 사이의 구조를 비교해서 그 구조이 맞다면 통과, 구조가 맞지 않다면 에러로 처리됩니다. SquareConfig는 모든 속성에 대해 옵셔널이므로 구조가 서로 호환된다고 판단되어 에러로 처리되지 않습니다. 이를 일반적인 타입 호환성(구조적 타이핑, structural typing) 검사라고 합니다. 그런데 다음 코드는 에러입니다.

let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);

SquareConfig는 모든 속성에 대해 옵셔널이므로 문제가 없어야 합니다. 하지만 SquareConfig 타입은 매우 특수한 경우로 처리됩니다. SquareConfig는 모든 속성이 옵셔널입니다. 이처럼 모든 속성이 옵셔널일 경우에는 반드시 속성 중 하나라도 같은 속성이 존재해야 합니다. 이런 검사를 약한 타입 감지(Weak Type Detection)라고 합니다.

TypeScript, 배열을 튜플로 …

다음 코드에서 객체는 배열입니다.

const args = [8, 5];

그래서 다음 코드는 에러입니다.

const angle = Math.atan2(...args);

에러의 내용은 “확산 인수는 튜플 유형을 가지거나 나머지 매개 변수로 전달되어야 합니다.”. 그럼 args를 배열이 아닌 튜플로 만들어줘야 합니다. 방법은 아래와 같습니다.

const args = [8, 5] as const;

as const는 그 구성 맴버까지 readonly로 만들어주는 것입니다. 튜플은 그 구성 항목의 개수가 고정이여야하므로 as const를 붙여 튜플로 만든다라는 표현은 합리적입니다.