react hook에 typescript를 적용하면서 generic 타입에 대해서 아직 제가 잘 모르고 있었다고 깨닫게 되었습니다.
그래서 새로 알게 된 내용을 기록해두고자 합니다.
1. generic의 extends가 곧 최종적인 타입은 아니다.
extends는 단순 generic 타입의 범위를 제한시켜주는 역할을 합니다. 제한시킨다는 것은 extends 오른쪽의 조건을 만족하거나 포함해야한다는 것입니다. 완벽히 동일할 필요는 없습니다. 예를 들어 아래의 예시 코드에서는 제네릭 타입 T는 완전히 Ttest와 동일한 타입일 필요는 없되, Ttest의 타입을 만족, 즉, 포함해야 한다는 것입니다.
type Ttest = {
result: string;
}
<T extends Ttest> ...
위를 바탕으로 제네릭T 자리에는 다음과 같은 타입도 들어올 수 있겠습니다. {result:string, level:number} 타입이 T제네릭이고 T제네릭이 Ttest를 만족시켜야 하는데 Ttest 타입의 {result: string} 타입을 포함하고 있기 때문에 extends 조건을 만족하게 되는 것입니다.
type Tsomething<T extends Ttest> = T
const something:Tsomething<{result:string, level: number}> = {
result: 'perfect',
level: 3
}
이를 함수에도 활용해보겠습니다.
type TforTestFunc = {
userId: string;
password?: string;
}
const testFunc = <T extends TforTestFunc>(x:T) => {
return x
}
testFunc({userId:"k", password: "kkk", "overflow": false})
testFunc은 T extends TforTestFunc 덕분에 최소한 type TforTestFunc은 포함하고 있는 상황입니다. 제네릭을 호출부에서 따로 선언하지 않아도 말이죠(이미 testFunc이 선언될 때 타입 extends가 되어있음). 이 때 "overflow":false 프로퍼티를 추가적으로 인수로 넣어보겠습니다. 그러면 에러를 발생시킬까요?

보시다시피 에러는 발생하지 않습니다. 이미 extends 조건인 TforTestFunc type은 충족한 상태에서 추가적으로 넣은 것이기 때문입니다. overflow에 대한 타입은 알아서 추론을 통해 타입지정이 된 모습을 볼 수 있습니다.
1-1. extends는 부모의 옵션을 필수 프로퍼티로 만들 수 있다.(Partial -> Required)
interface Iabsolute {
headers: string[];
color?: string;
item?: string[]
depth?: number;
}
interface Ichildren extends Iabsolute {
headers: string[];
color: string;
person: number;
}
const somethingHappy: Ichildren = {
headers: ['hi'],
person: number,
// Property 'color' is missing in type '{ headers: string[]; }' but required in type 'Ichildren'
}
extends는 단순 상속뿐만 아니라 다양한 기능을 할 수 있습니다. 예를 들어 Iabsolute 인터페이스에서 color는 옵션이지만 Ichildren에서는 옵션을 빼면서 필수 프로퍼티로 만들 수 있습니다.
2. react custom hook 에 적용시켜 보기
아래에서 다룰 내용은 위에서 다룬 함수에서 타입이 돌아가는 형태와 아예 99%유사한 내용입니다. 다른 점은 react hook에 적용시켰다는 점일 뿐이죠. useInput이라는 input 커스텀 훅을 만들었습니다.
const useInput = <T extends TinitialValue>(initialValue: T): TuseInput => {
const [target, setTarget] = useState<T>(initialValue);
.
.
.
return [target, onChangeHandler, onClearHandler, onValidator];
};
export default useInput;
generic T extends 는 TinitialValue로 제한을 하되, 최종적인 T type에 대한 결정은 useInput이 호출되는 곳에서 initialValue가 하게 될 것입니다. useInput을 호출하는 곳에서 제네릭을 따로 선언 하지 않을 시 argument로 들어오는 값이 최종 T type을 결정하게 되는 것이죠.
// SignInPage
const [inputs, onChangeInputs, onClearInputs, onValidator] = useInput({
phoneNumber: '',
password: '',
});
// SignUpPage
const [inputs, onChangeInputs, onClearInputs, onValidator] = useInput({
phoneNumber: '',
nickname: '',
password: '',
passwordConfirm: '',
code: '',
});


최종적인 T type을 useInput 호출부에서 하게 되는 것을 확인할 수 있었습니다. 응용하여 여러 곳에 써먹을 수 있겠습니다.