타입스크립트(ZeroCho) lib,es5,d,ts 분석
forEach, map 제네릭 분석
forEach
interface Array<T>{
forEach(callbackfn:(value: T, index:number,array:T[])=>ConstrainDOMString, thisArg?:any):void;
}
[1,2,3].forEach((item)=>{console.log(item);})
1. 자바스크립트로만 생각했을때 아래 코드는 짐작해봤을때 콘솔에 1,2,3 이 찍힌다.
item이 number로 추론되어있다.
이처럼 string number, boolean을 다추론이 정상적으로 된다.
이렇게 자동으로 추론을 잘 하는 이유는 위 코드에서
value = item
array:T[] = [true,false,true]
interface Array<T>{
forEach(callbackfn:(value: T, index:number,array:T[])=>ConstrainDOMString, thisArg?:any):void;
}
const a: Array<number> = [1,2,3];
a.forEach((value)=>{console.log(value)})
['1','2','3'].forEach((value)=>{console.log(value);})
[true,false,true].forEach((value)=>{console.log(value);})
처음 Array<T>가 number로 타입이 정의되면 이후 T들도 number가 되고 string이면 나머지 T들도 string이된다.
코드를 작성할때는 어떤 타입이 올지 모르지만 실행이되면 어떤게 들어올지 알때 제네릭을 사용한다.
제네릭을 사용하지않으면
function add(x:string|number, y:string|number){}
add('1',2)
add(1,'2')
이렇게 문자열과 숫자열이 같이 나와있는걸 막을 수 없다.
그래서 제네릭을 사용해서 같은 타입끼리만 가능하게 할 수 있다.
function add<T>(x:T, y:T){retrun x}
add('1','2')
add(1,2)
add(true,false)
타입스크립트는 추론을 x, y중 타입이 있는지 확인하고 T가 number가 되면 나머지도 number로 추론한다.
직접 타입을 지정해주는건 타입스크립트가 제대로 타입을 지정해주지 못할때 사용한다.
map
interface Array<T> {
map<U>(callbackfn:(value:T,index:number, array:T[])=>U, thisArg?:any):U[]
}
const strings = [1,2,3].map((value)=>value+1); // [2,3,4] number[]
filter 제네릭 분석
interface Array<T> {
filter<S extends T>(predicate: (value: T, index: number, array: T[])=>value is S, thisArg?:any):S[];
}
const filtered = [1,2,3,4,5].filter((value)=>value % 2);
⬇️
filter<number extends number>(predicate: (value: number, index: number, array: number[])=>value is number, thisArg?:any):number[];
filtered는 number로 추론된다. 고로 T는 number가 된다.
제네릭 분석은 찾기다 왜 filtered가 number인지 T를 진짜 타입으로 찾아가면 풀린다.
string인 것만 필터하고싶은데 filtered의 타입추론이 string | number로 나와서 string인것만 분리할 수 없다.
이럴경우 왜 추론을 못하는지? 추론을 잘 하게하려면 어떻게 해야하는지 생각해봐야한다.
filter<S extends string | number>(predicate: (value: T, index: number, array: T[])=>value is S, thisArg?:any):string | number[];
결국에는 string | number [] 로 되기때문에 추론도 string | number가 되는거다.
해결방법
interface Array<T> {
filter<S extends T>(predicate: (value: T, index: number, array: T[])=>value is S, thisArg?:any):S[];
}
// S를 원하는 타입으로 바꿔서 predicate를 똑같이 만들어준다.
const predicate = (value: string | number): value is string => typeof value === 'string';
const filtered = ['1',2,'3',4,'5'].filter(predicate);
이렇게 해주면 filtered의 타입추론은 string[]으로 나온다.
forEach 타입 직접 만들기
1. interface를 만든다.
2. forEach 메서드 선언을 하고 리턴을 사용안한다면 void로 처리한다.
// 1. interface를 만든다.
interface Arr {
// 2. forEach 메서드 선언을 하고 리턴값 정해진게 없다면 void로 처리한다.
forEach():void
}
3. 콜백 자리를 만든다.
4. item의 타입 정의를 해준다.
// 1. interface를 만든다.
interface Arr {
// 2. forEach 메서드 선언을 하고 리턴값을 사용안한다면 void로 처리한다.
// 3. 콜백 자리를 만든다.
// 4. item의 타입 정의를 해준다.
forEach(callback:(item:number)=> void ):void
}
const a: Arr = [1,2,3];
a.forEach((item)=>{
console.log(item);
});
a.forEach((item)=>{
console.log(item);
return '3';
})
이렇게 하면 에러는 사라진다 그치만 forEach함수는 여러가지가 들어올 수 있어서 b를 만들어보면 에러가 난다..
에러는 string형식은 number형식에 할당할 수 없다고 나온다.
interface Arr {
forEach(callback:(item:string)=> void ):void
}
item을 string으로 변경을하면 b의 에러는 사라지지만 이번엔 a에 에러가 발생한다.
interface Arr {
forEach(callback:(item:string | number)=> void ):void
}
이렇게 해주면 에러가 발생하지 않는다. 다만 이렇게 하면 조금만 복잡해져도 에러가 발생할 수 있다.
interface Arr<T> {
forEach(callback:(item:T)=> void ):void
}
const a: Arr<number> = [1,2,3];
a.forEach((item)=>{
console.log(item);
});
a.forEach((item)=>{
console.log(item);
return '3';
})
const b: Arr<string> = ['1','2','3'];
b.forEach((item)=>{
console.log(item);
item.charAt(3)
});
b.forEach((item)=>{
console.log(item);
return '3';
})
제네릭을 넣어서 사용하면 에러를 없애고 타입을 지정할 수 있다.
map 타입 직접 만들기
interface Arr<T> {
forEach(callback:(item:T)=> void ):void
}
const a: Arr<number> = [1,2,3];
const b = a.map((v)=> v+1)
b의 타입을 추론한다.
1. 함수를 만든다.
interface Arr<T> {
map():void
}
2. 콜백함수를 넣는다.
interface Arr<T> {
map(callback:(v)=> void):void
}
3. 제네릭을 추가한다.
interface Arr<T> {
map(callback:(v: T)=> T):T[];
}
const a: Arr<number> = [1,2,3];
const b = a.map((v)=> v+1)
4. 여러 조건을 만족하게 수정한다.
interface Arr<T> {
map<S>(callback:(v: T)=> S):S[];
}
const a: Arr<number> = [1,2,3];
const b = a.map((v)=> v+1);
const c = a.map((v)=>v.toString());
toString의 문자 타입을 만족하기 위해 S를 추가한다.
interface Arr<T> {
map<S>(callback:(v: T)=> S):S[];
}
const a: Arr<number> = [1,2,3];
const b = a.map((v)=> v+1);
const c = a.map((v)=> v.toString());
const d = a.map((v)=> v % 2 === 0);
만약 인덱스를 추가하게된다면
interface Arr<T> {
map<S>(callback:(v: T, i: number)=> S):S[];
}
const a: Arr<number> = [1,2,3];
const b = a.map((v, i)=> v+1);
const c = a.map((v)=> v.toString());
const d = a.map((v)=> v % 2 === 0);
그때그때 추가해서 수정해주면 된다.
여러 조건을 만족하는 타입의 map
interface Arr<T> {
map<S>(callback:(v: T, i: number)=> S):S[];
}
const a: Arr<number> = [1,2,3];
const b = a.map((v, i)=> v+1);
const c = a.map((v)=> v.toString());
const d = a.map((v)=> v % 2 === 0);
const e: Arr<string> = ['1','2','3'];
const f= e.map((v)=> +v)
filter 타입 직접 만들기
interface Arr<T> {
filter(callback:(v: T)=> boolean):T[];
}
const a: Arr<number> = [1,2,3];
const b = a.filter((v)=> v % 2 === 0); // [2] number[]
interface Arr<T> {
filter(callback:(v: T)=> boolean):T[];
}
const a: Arr<number> = [1,2,3];
const b = a.filter((v)=> v % 2 === 0); // [2] number[]
const c: Arr<number|string> = [1,'2',3,'4',5];
const d = c.filter((v)=> typeof v === 'string') //['2','4'] string[]
이렇게 되면 b는 타입이 정상적으로 추론되는데
문제는 d가 string| number[]가 되버린다.
interface Arr<T> {
filter<S>(callback:(v: T)=> v is S):S[];
}
const a: Arr<number> = [1,2,3];
const b = a.filter((v):v is number=> v % 2 === 0); // [2] number[]
const c: Arr<number|string> = [1,'2',3,'4',5];
const d = c.filter((v):v is string=> typeof v === 'string') //['2','4'] string[]
타입가드를 사용하면서 결과값이 다 T가 아닌 S제네릭을 추가하면 d의 타입추론이 string[]으로 바뀐걸 볼 수 있다.
그치만 T는 S일 수 없다고 에러가 뜬다.
넓은 타입을 좁은타입으로 줄여줄수 있는데 그럴때 <S extends T>를 사용해서 S는 T의 부분집합이라는걸 정의해서 사용할 수 있다.
interface Arr<T> {
filter<S extends T>(callback:(v: T)=> v is S):S[];
}
const predicate = (v:string|number): v is number => typeof v === 'number'
const e = c.filter(predicate)
위 예제처럼 분리해서 사용하면 조금은 가독성이 약간 좋아보일 수 있다.
공변성과 반공변성
function a(x:string): number {
return +x;
}
a('1'); // 1
type B = (x: string) => number | string
// 리턴값은 더 넓은 타입으로 대입이 가능하다.
const b: B = a;
리턴값은 좁은타입에서 더 넓은 타입으로 대입이 불가능하다. 넓은 타입으로만 대입 가능하다
function a(x:string | number): number {
return +x;
}
type B = (x: string) => number ;
// 리턴값은 더 넓은 타입으로 대입이 가능하다.
const b: B = a;
매개변수는 리턴값과는 반대로 좁은타입으로 대입이 가능하고 넓은 타입으로는 대입이 불가능하다.
리턴값은 넓은 타입으로 대입이 가능하고 매개변수는 좁은타입으로 대입이 가능하다
오버로딩
같은코드를 여러번 선언하는걸 오버로딩이라 한다.
declare function add(x:number, y:number) :number
declare function add(x:number, y:number, z:number) :number
add(1,2)
add(2,3,4)
add함수호출 두번을 한번에 처리하는걸 모르겠을땐 그냥 각각 써주면된다.
한번에 해결하는 방법으로는 옵셔널을 이용하면된다.
declare function add(x:number, y:number, z?:number) :number
add(1,2)
add(2,3,4)
타입스크립트는 건망증이 심하다(+에러 처리법)
interface Axios{
get():void
}
interface CustomError extends Error{
response?:{
data:any;
}
}
declare const axios: Axios
try {
await axios.get();
} catch (err: unknown) {
console.error((err as CustomError).response?.data);
//위에서 err가 뭔지 알려줬으나 바로 밑에서 또 err를 적으면 커스텀인지 알수 없다.
}
as를 사용 했다면 변수에 담아서 사용하면 코드마다 as를 안넣을 수 있다.
interface Axios{
get():void
}
interface CustomError extends Error{
response?:{
data:any;
}
}
declare const axios: Axios
try {
await axios.get();
} catch (err: unknown) {
const customError = err as CustomError
console.error(customError.response?.data);
//위에서 err가 뭔지 알려줬으나 바로 밑에서 또 err를 적으면 커스텀인지 알수 없다.
customError.response.data
}
as를 사용하는건 최대한 지양해야 한다. 그리고 이렇게 사용하면 실수가 무조건 날 수도 있고 커스텀에러가 아니라 다른에러가 생길 수 도 있기 때문에 조건문을 사용해줘야 한다.
interface Axios{
get():void
}
class CustomError extends Error{
response?:{
data:any;
}
}
declare const axios: Axios
try {
await axios.get();
} catch (err) {
if(err instanceof CustomError)
console.error(err.response?.data)
err.response?.data
}
as를 사용하지 않는게 제일 좋은 코드고 unknown이 있으면 타입가드를 사용해주는게 좋다!
인터페이스는 자바스크립트에서 사라지기 때문에 유지시키고 싶으면 class를 사용해준다.