📌Map과 ForEach 함수의 차이점

map - 배열 각 요소에 콜백함수를 실행한 후 새로운 배열을 반환해줌

foreach - 배열 각 요소에 콜백함수 실행만 해줌, 반환값 X


Map과 제네릭


function map(arr:unknown[], callback:(item: unknown)=> unknown ): unknown[]{
     let result = [];
     for(let i=0; i<arr.length; i++){
        result.push(callback(arr[i]))
   }
}

 

map함수는 배열콜백함수 두 가지를 인수로 받는다.

위는 두 가지 인수 모두 unknown, 반환값 역시 unknownd으로 임시 설정해놓은 상태.

 

map([1,2,3], (item) => item * 2);

이 경우 매개변수 arr와 map함수 반환값 타입은 number[ ] , 콜백함수의 인수와 반환값 모두 number타입이다.

 

그래서 위 함수 식을 제네릭 함수로 바꾸면


function map<T>(arr:T[], callback:(item: T)=> T): T[]{
     let result = [];
     for(let i=0; i<arr.length; i++){
        result.push(callback(arr[i]))
   }
}

 

하지만 이렇게 제네릭 타입 변수를 하나만 설정하면 문제가 생긴다.

 

📌만약에 map([1,2,3] , (it)=>it.toString()) 이라면?

매개변수 arr와 콜백함수 인수는 number, 반환값은 string 타입으로 사용되는 타입이 두 가지 이상이다.

그렇다면 타입 변수를 두 개 이상 써서 반환값의 타입을 분리해야 한다.


function map<T,U>(arr:T[], callback:(item: T)=> T): U[]{
     let result = [];
     for(let i=0; i<arr.length; i++){
        result.push(callback(arr[i]))
   }
}

 

📌또 만약에 이렇게 map 함수를 호출했다면?

 
   map([100,"string"], (it)=> it.toString())
 

 

인수로 들어가는 배열 함수는 넘버타입과 스트링타입이 섞여있다.

이 때 map 함수의 타입 정의는 다음과 같다.

 
     function map<string | number , string>(arr: (string | number)[], callback:(item: string | number) => string ) : string []
 

 

 


ForEach와 제네릭

forEach 함수는 반환값은 없기때문에 매개변수로 들어가는 배열과 콜백함수의 매개변수 타입만 정리해주면 된다.

 


function forEach<T>(arr: T[], callback: (item:T) => void){
    for(let i = 0; i < arr.length; i++){
    	callback(arr[i])
    }
}

 

제네릭


function func(value:any){
	return value;
}

let num = func(10); //any 타입
let str = func("string") //any 타입

 

any타입인 변수에 다른 타입의 값을 넣어도 any타입이 된다.

반환값 value를 기준으로 타입 추론이 되기때문이다.


    ex.
    let Test:any
    Test=10;
    Test //any 타입
 

 

Test 변수를 any타입으로 설정한 후 다른 타입을 재할당해도 any타입인것을 생각해보면 된다.

 

이렇게 만들어진 함수에

num.toUpperCase();를 실행해도 오류를 감지하지 못한다.

 

그래서 필요한 것이 모든 타입의 값을 적용할 수 있는 제네릭 함수이다.

 

제네릭 기본 문법


function func(value){
	return value;
}

 

여기서 아래처럼 매개변수의 타입만 설정해주면 타입스크립트 문법이다.


function func(value:string){
	return value;
} // func 의 타입은 string으로 자동 추론

 

꺽쇠를 열고, 타입변수 T를 선언한다.

매개변수와 반환값의 타입에도 타입변수 T를 선언한다.

 

☘️ 타입변수 = 타입 파라미터 = 제네릭 타입변수 = 제네릭 타입 파라미터


function func<T>(value:T):T{
	return value;
}

 

이렇게 선언된 타입변수 T는 범용적이고 일반적인 타입으로 추론되기때문에

원하는 타입이 있다면 구체적으로 설정해야한다.


function func<T>(value:T):T{
	return value;
}

let ten = func(10) // number 타입
let ten2 = func<10>(10) // number literal 타입
let arr = func([10,100]) //배열타입
let arr2=func<[number,number]>(10,100) //튜플타입

 


제네릭 적용 사례

✔️ 타입변수의 타입이 2개 이상 필요할 때

 

제네릭함수를 하나의 타입으로만 사용 할 경우 예시, 아래와 같이 오류메시지 발생!


function func<T>(a:T,b:T) {
	return [b,a]
}

let arr = func(["string",100])
//타입이 통일되지 않아 오류메시지 발생

 

아래와 같이 타입변수를 두 개로 분리한다.

 


function func<T,U>(a:T,b:U) {
	return [b,a]
}

 

 

✔️다양한 배열 타입이 인수가 될 때


function ArrayFirst<T>(data:T){
	return data[0]
}


let num = ArrayFirst([10,11,12]) //number 타입
let str = ArrayFirst(["string",100,"text",121]) // number | string 타입이 된다.

 

number | string 타입으로 추론되었기때문에 return문에서는 data[0] 자리에 숫자가 올지 문자가 올지 알 수 없다.

좀 더 확실히 하고싶다면 튜플타입을 활용한다.

 

 

✔️다양한 배열 타입이 인수가 되는 경우, 타입을 좀 더 정확히 정하고 싶을 때


function ArrayFirst<T>(data:[T, ...unknown[]]){
    return data[0];
}

let str = ArrayFirst(["string",100,"text",121]) // string 타입

 

 

✔️타입 변수 제한하기 ( 확장 {extends} 활용 )


function getLength<T>(data:T){
      return data.length; // 반환값은 any타입으로 추론되어 오류 발생
}

function getLength<T extends {length:number}>(data:T){
     return data.length;
}

 // length 프로퍼티를 갖고있는 객체타입을 확장한 것이 T, 즉 T는 length:number 를 갖고있는 객체의 서브타입이 된다.

 


let num = getLength([1,2,3]) //3
let str = getLength("test") //4
let obj = getLength( {length:100} ) // 100
let unde = getLength(nudefined) //오류

 

클래스

동일한 모양의 객체를 더 쉽고 간결하게 만들어주는 "틀"의 역할.

필드, 생성자, 메서드 세 구간으로 나눌 수 있다. 

class Employee {
    // 필드
    name: string;
    age:number;
    position:string;

    //생성자
    constructor( name: string , age:number , position:string ) {
        this.name=name;
        this.age=age;
        this.position=position;
    }

    //메서드
    work(){
    	console.log('working')
    }
}

let employee = new Employee("사람1",20,"개발자");

 

  • 클래스는 타입으로도 사용 가능
let employee: Employee = {
    name:"이름",
    age:20,
    position:"개발자",
    work(){ }
}

 

  • 상속 가능 ( super의 등장 )
class MoreEm extends Employee {
    officeNumber:number;

    constructor(
        name:string,
        age:number,
        position:string,
        officeNumber:number
    ){
        super(name,age,position); //최상단에 쓴다
        this.officeNumber = officeNumber;
    }
}

 

위에 만들어놨던 Employee 클래스를 상속받은 새로운 MoreEm 클래스를 만들었다.

super(원본 클래스의 프로퍼티들) 을 생성자함수 최상단에 꼭 넣어줘야한다.

 

 

  • 접근제어자

public - 어디에서나 접근 가능 (굳이 쓰지 않아도 자동 설정됨)

private - 클래스 내부에서만 접근 가능

protected - 클래스 내부 & 파생 클래스에서 접근 가능

class Employee {
	private name:string;
	protected age:number;
	public position:string;
	constructor(name:string,age:number,position:string){
			this.name=name;
			this.age=age;
			this.position=position;
		}

	work(){
		console.log(`${this.name} 일함`) //name은 프라이빗이라서 class내부에서만 접근 가능
		console.log(`${this.age}`)
	}
}

class more extends Employee {
	func(){
		console.log(`${this.age}`) // age는 protected 라서 class내부와 파생class에서 접근 가능
	}
}

const employee = new Employee("사람1",20,"developer")

employee.position = "디자이너" //position은 퍼블릭이라서 어디에서나 접근 가능

 

 

☘️코드를 조금 더 간단하게!

  • 필드 생략 가능
class Employee {
	constructor(public name:string, private age:number, protected position:string){}
	work(){
		console.log(`나이는 ${this.age} 입니다`) // private는 내부에서만 접근 가능
	}
}

 

생성자함수 매개변수 넣는 곳에 접근제어자를 설정하면 자동으로 필드가 생성된다.

필드는 중복으로 설정할 수 없으므로 원래 필드가 있던 곳은 삭제 가능

그리고 생성자함수 내부의 this.필드 = 매개변수; 역시 자동으로 생성되기때문에 삭제 가능

 

 

  • interface를 활용해 class 설계하기
interface Inter {
	name:string;
	movespeed:number;
	move():void;
}

class Character implements Inter {
	constructor (
		public name:string,
		public movespeed:number
	) {}

	move(){
		console.log(`${this.name}의 속도는 ${this.movespeed}`)
	}
}

 

interface를 사용해서 class에 사용될 필드와 메서드를 정의할 수 있다.

 

인터페이스  - 타입 별칭처럼 타입에 이름을 지어주는 문법이다.

interface Test {
    name:string;
    age:numer;
}

type Test = {
    name:string;
    age:numer;
}

 

타입 별칭과는 등호 표시 빼고는 문법이 같다.

타입 별칭이 갖고있는 기능, 예를 들면 선택적 프로퍼티, 읽기전용 프로퍼티 역시 설정할 수 있다.

 

  • 메서드 오버로딩

호출 시그니처를 이용해 오버로딩 구현도 가능하다.

함수 타입 표현식은 안됨

type Foo = (a:number) => number; //함수타입표현식

type Foo = { //호출시그니처
	(a:number) : number
}


interface Test {
    foo(a:number):void;
    foo(a:number, b:number):void;
}

 

타입별칭과 다른 점

1.union 이나 intersection 타입을 정의할 수 없다.

interface Test = {
	name:string;
} | number

 

이런건 오류발생, intersection으로 만든 타입으로 대수타입을 만들고 싶다면

type Type1 = boolean | Test

 

이런식으로 타입별칭을 활용해야한다.

 

2.인터페이스 확장

하나의 인터페이스를 다른 인터페이스가 상속받아 중복코드를 줄일 수 있다.

interface Animal {
	name:string;
	age:number;
}

interface Dog {
	name:string;
	age:number;
	breed:string;
}

interface Cat {
	name:string;
	age:number;
	color:string;
}

interface mouse {
	name:string;
	age:number;
	height:number;
}

 

각각 name과 age 프로퍼티가 중복된다.

Animal 인터페이스를 기준으로 Dog, Cat, mouse 인터페이스들이 파생되었다고 볼 수 있다.

그래서 Animal 인터페이스를 확장시킨다.

 

interface Animal {
	name:string;
	age:number;
}

interface Dog extends Animal{
	breed:string;
}

interface Cat extends Animal{
	color:string;
}

interface mouse extends Animal{
	height:number;
}

 

이렇게 확장을 하면서 프로퍼티를 재정의할 수도 있다.

interface Dog extends Animal{
	name: "강아지"
	breed:string;
}

 

☘️주의점

원본 프로퍼티값을 A, 재정의한 프로퍼티값을 B라고 할 때 

A가 B의 슈퍼타입이어야한다.

 

  • interface를 사용한다면 type 별칭 역시 확장 가능
type Animal = {
	name:string;
	age:number;
}

interface Dog extends Animal {
	breed: string;
}

 

  • 다중확장은 콤마( , )를 사용한다.
interface DogCat extends Dog,Cat {
	
}

 

3.선언 합침

타입별칭은 같은 스코프 내에서 동일한 이름을 쓸 수 없다.

하지만 인터페이스는 동일 이름 사용 가능하며, 이러한 인터페이스들은 자동으로 합쳐진다.

 

interface Animal {
	name:string;
}

interface Animal {
	age:number;
}

let animal:Animal = {
    name:"동물",
    age:100
}

 

 

☘️주의점

동일한 인터페이스명의 동일한 프로퍼티 타입을 다르게 정의하면 오류발생!

interface Animal {
    name:string;
}

interface Animal {
    name:number; //오류발생
    age:number;
}

오버로딩 - 함수는 하나, 매개변수의 갯수 or 타입에 따라 다르게 구현되도록 하는 것

함수의 선언부와 구현부로 나눠서 살펴보자면 

 

☘️선언부 (오버로드 시그니쳐) - 다양한 버전을 나타냄

 
     function foo(a:number):void; //foo의 버전1
     function foo(a:number,b:number,c:number):void; //foo의 버전2
 

 

☘️ 구현부

 
 
   function foo(a:number, b?:number, c?:number) {
        if(typeof b === "number" && typeof c === "number") {
            console.log("버전2일 때의 구현부")
        } else {
            console.log("버전1일 때의 구현")
        }
    }
 

 

이 때 버전1과 버전2는 매개변수가 1개, 3개이므로 

 
   foo(1,2) //오류
 

 

 

사용자 정의 타입가드

참,거짓을 반환하는 함수를 이용해 타입 좁히기를 할 수 있게 해준다.

type Dog = {
	name:string;
	isBark:boolean;
}

type Cat = {
	name:string;
	isScratch:boolean;
}

type Animal = Dog | Cat;

function warning(animal:Animal) {
	if("isBark" in animal) {
     	console.log('개타입')
    } else if ("isScratch" in animal) {
    	console.log('고양이타입')
    }
}

 

물론 in 타입가드를 사용해도 좋지만, 직관적이지 않을뿐더러 타입의 프로퍼티 네임이 변경되면 오류 발생 가능성이 있기 때문에 

//dog타입인지 확인하는 타입 가드
function isDog(animal:Animal): animal is Dog{
	return (animal as Dog).isBark !== undefined
}

//cat타입인지 확인하는 타입 가드
function isCat(animal:Animal): animal is Cat {
	return (animal as Cat).isScratch !== undefined
}

 

 animal is Dog/Cat의 의미 - isDog / isCat 함수가 true를 반환하면 Dog / Cat 타입임을 보장한다.

리턴문에서 as 단언문을 쓴 이유는 isBark는 Animal타입과 Cat타입에서 찾을 수 없는 프로퍼티라서 오류가 발생하기때문이다.

function warning(animal:Animal){
	if(isDog(animal)){
		console.log('개타입')
	} else if (isCat(animal)) {
		console.log('고양이타입')
	}
}

 

결과적으로 warning 함수에서의 타입 좁히기는 이렇게 바꿔쓸 수 있다.

+ Recent posts