리액트와 유사한 useState() 함수를 만드는데 useState()의 매개변수인 initialValue의 타입을 어떻게 정의할 지 고민이 있었다.
상태는 number, string도 되고 심지어 배열, 객체까지 여러 종류의 타입이 될 수 있는데..? 그럼 any를 써야하나..?
그러나 any를 사용하는 것은 좋지 않다고 한다.
any를 사용하면 왜 안 좋을까?
→ 타입 안정성을 보장 받지 못함
→ 컴파일 타임에 오류를 미리 발견하지 못해 이후 런타임에서 문제 발생 위험 증가
→ 타입스크립트를 사용 안하는 거와 다름 없음!
function useState(initialValue: any){
if (_value === undefined) _value = initialValue;
const setState = (newValue: any) => {
_value = newValue;
render(currentParent, currentComponent);
}
return [_value, setState];
}
위 코드의 경우 any타입으로 모든 타입을 매개변수로 받을 수 있지만 반환하는 타입이 매개변수의 타입이라고 보장할 수 없다.
이를 해결하기 위해 Generic
을 사용할 수 있다.
Generic
은 TypeScript에서 함수나 클래스, 인터페이스가 다양한 타입을 처리할 수 있도록 하는 방법이다.
Generic
을 사용하면 구체적인 타입에 의존하지 않고, 여러 타입을 유연하게 사용할 수 있는 코드를 작성할 수 있다!
function useStateNumber(value:number):[number, (a:number)=>void]{
const fn = (a:number)=>{};
return [value,fn];
}
function useStateString(value:string):[string, (a:string)=>void]{
const fn = (a:string)=>{};
return [value,fn];
}
function useStateObject(value:object):[object, (a:object)=>void]{
const fn = (a:object)=>{};
return [value,fn];
}
위 코드의 각 함수들은 내부 로직은 동일하지만 매개변수와 반환되는 타입만 다르다.
여기에 Generic
을 적용해 재사용성을 높일 수 있다.
function useState<T>(value:T):[T, (a:T)=>void]{
const fn = (a:T)=>{};
return [value,fn];
}
const [num, setNum] = useState(1);
const [arr, setArr] = useState([1,2,3]);
다음과 같이 꺾쇠 괄호(<>)와 대문자 T 변수를 지정해 사용할 수 있다.
변수 이름은 어떤 것으로 T가 아니어도 되지만 관습적으로 대문자 알파벳 한글자로 처리한다.
Generic
은 함수나 클래스의 선언 시점이 아니라, 사용 시점에 타입이 결정된다
Generic
을 통해 코드에 선언한 타입을 변수화 하고, 나중에 타입을 정하는 식으로 유연하게 사용이 가능하다.
generic을 활용해 다음과 같이 변경해볼 수 있었다.
export function useState<T>(initialValue: T): [T, (value: T) => void] {
if (_value === undefined) _value = initialValue;
const setState = (newValue: T) => {
_value = newValue;
render(currentParent, currentComponent);
};
return [_value, setState];
)
Generic
에서 extends
를 사용해 제약조건을 걸 수 있다.
function printName<T>(person: T) {
console.log(person.name); // BugFinder: Property 'name' does not exist on type 'T'.
}
printName({ name: "Alice" });
위와 같이 generic을 활용한 간단한 함수가 있다.
위 코드에서 T는 어떤 타입이든 받을 수 있는데, T가 'name' 속성을 반드시 가진다는 보장이 없기 때문에 오류가 발생한다.
extends
를 사용하면 이를 해결할 수 있다.
type Person = {
name: string;
}
function printName<T extends Person>(person: T) {
console.log(person.name);
}
printName({ name: "Alice" }); // 정상
printName({ age: 25 }); // 오류: 'name' 속성이 없어서 오류 발생
다음과 같이 Person type을 T가 상속 받아서 T에는 항상 name 속성을 포함해야 한다.
→ 타입 안정성이 보장된다.
타입 안정성을 보장하기 위해 extends
도 기억해두고 다양한 상황에 사용해보자!