강박적으로 제너릭을 쓸 필요는 없다. 특히, 원시값 타입만 사용한다면 대부분 제약을 걸지 않아도 된다.
제너릭
타입간에 중복을 제거하기 위해 제너릭을 사용
interface Person<N, A> {
type: 'human',
race: 'yellow',
name: N,
age: A,
}
interface Zero extends Person<'zero', 28> {}
interface Nero extends Person<'nero', 32> {}
// Array 타입도 제너릭
// 제너릭이 없었다면 스트링배열, 넘버배열을 따로 타입을 선언해줬어야 됬을것
interface Array<T> {
[key: number]: T,
length: number
//...
}
표기는 <> 로 하며 인터페이스 이름 바로 뒤에 위치
<> 안에 타입 매개변수를 넣어서 사용
제너릭 타입으로 타입간 중복을 줄이고 재사용성을 높힘
제너릭 표기 위치
함수에서 함수 선언문이냐 표현식이냐에 따라 제너릭 표기 위치가 달라짐
함수는 제너릭표기가 인자 앞에 위치한다고 생각
const personFactoryE = <N, A>(name: N, age: A) => {
...
}
function personalFactoryD<N,A>(name: N, age: A) {
...
}
interface 이름<타입 매개변수들> {...}
type 이름<타입 매개변수들> = {...}
class 이름<타입 매개변수들> {...}
function 이름<타입 매개변수들>(...) {...}
const 함수이름 = <타입 매개변수들>(...) ⇒ {...}
제너릭 기본값
타입 매개변수에 기본값을 사용할 수 있다.
interface Person<N = string, A = number> {
type: 'human',
race: 'yellow',
Name: N,
age: A
}
type Person1 = Person; // Person<string, number>
type Person2 = Person<number>; // Person<number, number>
type Person3 = Person<number, boolean>; // Person<number, boolean>
타입 추론
타입스크립트는 제너릭에 직접 타입을 넣지 않아도 추론을 통해 타입을 알아 낼 수 있다.
interface Person<N, A> {
type: 'human',
race: 'yellow',
Name: N,
age: A
}
const personFactoryE = <N, A = unknown>(name: N, age: A): Person<N,A> => ({
type: 'human',
race: 'yellow',
name,
age,
})
const zero = personFactoryE('zero', 28); // const zero: Person<string, number>
이처럼 타입스크립트가 추론을 통해 타입을 알아낼 수 있는 경우는 직접 <> 타입을 넣지 않아도 된다.
실제로도 직접 넣지 않는 경우가 더많음
타입스크립트 5.0 버전에서 상수 타입 매개변수 (const T)가 추가됨
상수 타입 매개변수
function values<T>(initial: T[]) {
return {
hasValue(value:T) { return initial.includes(value) }
}
}
const savedValues = values(['a','b','c']); // T는 string[]로 추론됨
savedValues.hasValue('x'); // T가 string으로 추론되기에 'x'를 넣어도 타입 오류가 나지 않음
❓T를 string 대신 'a' | 'b' | 'c' 같은 유니온으로 추론되게 하려면?
타입 매개변수 앞에 const 수식어를 추가하면 타입 매개변수 T를 추론할 때 as const를 붙인 값으로 추론됨
// TS v4.9
function values<T>(initial: readonly T[]) { //readonly 수식어로 정확한 타입을 추론하게끔 설정
//...
}
const savedValues = values(['a','b','c'] as const); // as const 접미사로 tuple로 만듬
savedValues.hasValue('x'); // T가 'a' | 'b' | 'c' 이므로 에러 발생
// TS v5.0
function values<const T>(initial: T[]) { // 타입 매개변수 앞에 const 수식어 사용
//...
}
const savedValues = values(['a', 'b', 'c']);
savedValues.hasValue('x'); // 타입 에러 발생
제너릭에 제약 걸기
타입 매개변수에는 제약(constraint)를 사용 가능
extends 문법으로 타입 매개변수의 제약을 표시
타입의 상속을 의미하던 extends 와는 사용법이 다르므로 구분!!
제약이 걸리면 제약에 어긋나는 타입은 입력할 수 없지만 제약보다 더 구체적인 타입은 입력 가능
이러한 점에서 제약은 기본값과는 다르다.
interface Example<A extends number, B = string> {
a: A,
b: B
}
type Usecase1 = Example<string, boolean>; // 타입 에러
type Usecase2 = Example<1, boolean> // number 보다 더 구체적인 타입 입력 가능
type Usecase3 = Example<number>
// 하나의 타입 매개변수가 다른 타입 매개변수의 제약이 될 수 도 있다.
interface Example<A, B extends A> {
//...
}
‼️ 자주 쓰이는 제약들
<T extends object> // 모든 객체
<T extends any[]> // 모든 배열
<T extends (...args: any) => any> // 모든 함수
<T extends abstract new (...args: any) => any> // 생성자 타입
<T extends keyof any> // string | number | symbol
제너릭 제약을 사용할 때 흔히 하는 실수
❌ 타입 매개변수와 제약을 동일하게 생각하는 것
먼저 타입 매개변수가 제약에 대입할 수 있는 타입인지를 따져보아야 한다.
강박적으로 제너릭을 쓸 필요는 없다. 특히, 원시값 타입만 사용한다면 대부분 제약을 걸지 않아도 된다.
interface VO {
value: any;
}
// ☠️ 타입 매개변수와 제약을 동일하게 생각해서 발생하는 실수
const returnVO = <T extends VO>(): T => {
return { value: 'test' }; // Type Error
}
// 제너릭을 제거하면됨
const returnVO = (): VO => {
return { value: 'test' };
}
T 는 정확히 VO가 아니라 VO에 대입할 수 있는 모든 타입을 의미
따라서 { value: string, another: string }도 T가 될 수 있다.
이러면 { value: string } 은 T가 아니다. 따라서 에러가 발생함.
인자값의 타입은 열려있고, 반환값의 타입은 닫혀있다. (공변성, 반공변성 참고)
// ☠️ 타입 매개변수 T 에 boolean 제약이 걸려있고, 함수의 매개변수도 T 타입
function onlyBoolean<T extends boolean>(arg: T = false): T { // 인자 타입에서 에러 발생
return arg;
}
// onlyBoolean을 유효하게 만들고 싶다면 간단하다. 제너릭을 쓰지 않으면 된다.
function onlyBoolean(arg: boolean = true): boolean {
return arg;
}
never 타입 때문, never는 모든 타입에 대입할 수 있으므로 never extends boolean은 참이다.