TS에서 타입추론을 하는 방법은 제네릭 덕분이다.
TS에서 제네릭 타입을 추론할 때 사용된 제네릭 타입 중 가장 첫번째로 알고 있는 값을 기준으로 보고 모든 제네릭 타입을 그 타입으로 추론한다.
(내생각) 제네릭은 약간 함수의 매개변수 같은 느낌이다.
제네릭으로 명시된 타입을 사용하는 밖에서 실제 값이 들어오면(인자) 제너릭 타입 안에서 제네릭 타입으로 명시된 값들에 실제 값의 타입이 들어간다(매개변수)
interface Array<T> {
forEach(
callbackfn: (value: T, index: number, array: T[]) => void,
thisArg?: any
): void;
}
// forEach
[1, 2, 3].forEach((value) => console.log(value));
["1", "2", "3"].forEach((value) => console.log(value));
하지만 TS가 타입추론을 잘 못한다면 타입을 알려줄 수도 있다.(타입 파라미티)
interface Array<T> {
map<U>(
callbackfn: (value: T, index: number, array: T[]) => U,
thisArg?: any
): U[];
}
// map
const strings = [1, 2, 3].map((value) => value.toString());
const numbers = [1,2,3].map<number>((v) => v*1)
먼저 [1,2,3]은 number[]이므로 T는 number임을 알 수 있고,
callbackfn의 return 값이 string 이므로 U도 string인 것을 알 수 있다.
💡 주의! 타입파라미터는 값뒤에 < > 이고, 값 앞에 < > 는 갖에 형변환이다.(as)같은 함수가 여러가지 방법으로 사용되는 경우에는 타입이 여러가지로 오버로딩 되어있다.
제네릭은 타입찾기다. 하나씩 비교하며 타입이 확정되면 그것을 기준으로 동일한 제네릭이 모두 같은 타입이 된다.
interface Array<T>{
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
filter(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): T[];
}
// T가 number가 되므로 S도 number가 되고
const filterfn1 = [1,2,3,4,5].filter(v => v%2)
// (string| number)[]으로 제대로 타입분석을 하지 못하고 있다. (T가 string| number이기 때문)
const filterfn2 = ["1","2","3",4,5].filter(v => typeof v === 'string')
// 아래 타입의 경우 제네릭이 T밖에없기 때문에 string| number를 바꿀 수 없다.
// 하지만 위에 타입경우 T는 고정되어있지만 S를 바꿀 수 있는 여지가 있기 때문에 위에 타입으로 추론해야한다.
// string extends string|number 이기 때문에 커스텀 타입가드를 이용해서 S를 string으로 고정해주면 원하는 타입을 얻을 수 있다.
const predicate = (value:string|number) : value is string =>typeof value === 'string'
const filterfn3 = ["1","2","3",4,5].filter(predicate)
하지만 여기서는 map에서 했던것 처럼 S 타입을 바로 지정해 줄 수 없다.
왜냐하면
filter<S extends T>(*predicate*: (*value*: T, *index*: number, *array*: T[]) => *value* is S, *thisArg*?: any): S[];
타입의 predicate 는 return 값이 value is S로 커스텀 타입가드이기때문에
["1","2","3",4,5].filter<string>(*v* => typeof *v* === 'string')
이렇게 해주면 return 값이 S로 타입가드(형식조건자)가 아니라서 error가 난다.
내가 쓸 타입을 interface로 만들어 주고 그 안에 메서드로 forEach를 만들어 본다
interface Arr<T>{
forEach(callbackfn : (item : T) => void): void
// 정답
// forEach(callbackfn: (value: number, index: number, array: Int8Array) => void, thisArg?: any): void;
}
const myForEach1 : Arr<number> = [1,2,3]
myForEach1.forEach((item) => console.log(item))
const myForEach2 : Arr<string> = ["1","2","3"]
myForEach2.forEach((item) => console.log(item))
내가 원하는 코드를 보고 타입을 짤 수 있어야 한다.
array의 원소들의 타입인 T는 알 수 있지만 map의 반환타입은 T와 다를 수 있기 때문에 map을 사용하는 순간 타입이 지정되는 새로운 제네릭 S가 필요하다
// T는 map을 사용하기 전에 타입지정, S는 사용하는 순간의 타입 지정
interface Arr<T> {
map<S>(callBackFn: (v: T) => S): S[];
}
const myMap: Arr<number> = [1, 2, 3];
const myMapResult1 = myMap.map((v) => v * 1);
const myMapResult2 = myMap.map((v) => v + "");
const myMapResult3 = myMap.map((v) => !!v);
const myMap2: Arr<string> = ["1", "2", "3"];
const myMapResult4 = myMap.map((v) => +v);
number | string이 T인데 number | string이 아닌 새로운 타입이 필요하므로 새로운 제네릭 S를 추가해준다(filter를 쓸 때 결정되므로 filter에 붙여준다)
이때 v 는 T인데 v is S 가되려면 S가 T의 부분집합이여야 가능하기 때문에 S extends T로 제네릭 S를 제한해준다.
interface Arr<T> {
filter<S extends T>(callbackFn: (v: T) => v is S): S[];
}
const a: Arr<number> = [1, 2, 3];
const myFilter = a.filter((v): v is number => v < 2); // [1]
const b: Arr<number | string> = [1, 2, "3"];
const myFilter2 = b.filter((v): v is string => typeof v === "string"); // ['3']
reduce타입 직접 만들기
interface Arr<T> {
reduce(callBackFn: (a: T, b: T) => T, init?: T): T;
reduce<S>(callBackFn: (a: S, b: T) => S, init?: S): S;
// 정답
// reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T): T;
// reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: readonly T[]) => T, initialValue: T): T;
// reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, initialValue: U): U;
}
const a: Arr<number> = [1, 2, 4, 5, 3];
const myReduce1 = a.reduce((a, b) => (a += b)); // 15
const myReduce2 = a.reduce((a, b) => (a += b), 10); // 25
const b: Arr<number | string> = [1, 2, "4", "5", 3];
const myReduce3 = b.reduce<number>((a, b) => {
return typeof b === "number" ? (a += b) : a;
}); // 6
const myReduce4 = b.reduce<number>((a, b) => {
return typeof b === "number" ? (a += b) : a;
}, 10); // 16
const c = [1, 2, "4", "5", 3];
const solReduce1 = c.reduce<number>((a, b, idx, arr) => {
return typeof b === "number" ? (a += b) : a;
}, 10); // 25
// 초기값 필수..?
const solReduce2 = c.reduce<number>((a, b, idx, arr) => {
return typeof b === "number" ? (a += b) : a;
}); // 25
공변성 반공변성은 함수간에 서로 대입할 수 있는지 유무를 따지는 것임
결론
리턴값은 더 넓은 타입으로 대입이 가능하다 (공변성)
매개변수는 리턴 값이랑 반대로 좁은 타입으로 대입이 된다. (반공변성)
// a를 b에 대입
function a(x: string | number): number {
return 0;
}
type B = (x: number) => number | string;
let b: B = a;
cf) 타입스크립트에서의 타입들은 기본적으로 공변성 규칙을 따르지만, 유일하게 함수의 매개변수는 반공변성을 갖고 있다. (tsconfig의 strictFunctionTypes 옵션이 true 일때의 기준)
한번에 내가원하는 타입을 모두 표현할 수 없으면 다 작성하자(오버로딩)^^
interface, class 안에서 모두 오버로딩 가능하다
cf) declare는 타입 정의만하고 함수 구현은 안해도 된다.(어디선간 해야함)
타입스크립트는 타입변환을 강제로 해줘도 밑에 바로 사용하려고 해도 안된다.
이럴경우 변수로 만들어서 사용해면 아래서도 사용할 수 있다.
interface Axios {
get(): void;
}
class CustomError extends Error {
response?: {
data: any;
};
}
declare const axios: Axios;
(async () => {
try {
} catch (err: unknown) {
console.error((err as CustomError).response?.data);
// 타입을 지정해준건 일회성이라 그 다음에 또 까먹는다
err.response?.data; // error msg : 'err' is of type 'unknown'.
// 그래서 변수를 만들어서 타입을 저장해준다. (as를 최대한 적게 쓰기 위함)
const customError = err as CustomError;
console.log(customError.response?.data);
// as 안쓰기(best)
if (err instanceof CustomError) {
const customError2 = err;
console.log(customError2.response?.data);
}
}
})();
const a = <T = unknown>(v: T): T => {
return v;
};
const c = a(3);
cf) inteface는 JS에서 사라지기 때문에 instanceof를 사용할 수 없다 그래서 class로 만들어줘야함