이번에는 타입스크립트에선 함수를 선언하는 사용하는 방법을 정리하였다. 최종적으로 아래 4가지 내용을 정리했다.
- 타입스크립트에서 함수를 선언하고 실행하는 다양한 방법
- 시그니처 오버로딩
- 다형적 함수
- 다형적 타입 별칭
타입스크립트에서 함수를 선언하는 가장 기본적인 방법이다.
function ( a : number , b : number ) : number {
return a + b;
}매개변수 타입과 반환 값 타입을 명시적으로 정의한 방법이다. 당연히 타입은 명시적으로, 암시적으로 선언할 수 있는데 보통 매개변수는 명시적으로 , 리턴 값은 암시적으로 추론하게 두는 것이 일반적이다.
매개변수를 암시적으로 타입 선언하는 경우의 대표적인 경우가 문맥적 타입화를 사용할 경우이다.
function ( a : number , b : number ) {
return a + b;
}타입스크립트의 함수 선언은 자바스크립트의 함수 선언과 완전히 동일하다. 단지 타입을 명시적으로, 암시적으로 선언하느냐의 차이가 있을 뿐이다.
const adder = function ( a : number, b : number ) : number {
return a + b;
}
// 이름을 붙여서 선언
function minus( a : number , b : number ) {
return a - b;
}
// 함수 표현식
const multiple = function ( a : number , b : number ) : number {
return a * b;
}
// 화살표 함수 표현식
const greeting1 = ( name : { name : string, greeting : string } ) : void => { console.log( name.name + name.greeting ) }
// 단축형 화살표 함수 표현식
const greeting2 = ( name : string ) => "hello" + name;당연히 인수를 적게 넣거나, 인수 타입을 다르게 넣는다면 오류가 발생한다.
/**
* 인수를 전달하지 않거나 잘못된 타입을 전달하면 오류 발생
* @error TS2322: Type 'number' is not assignable to type 'string'. 1_functionInTypesctipy.ts(22, 30): The expected type comes from property 'name' which is declared here on type '{ name: string; greeting: string; }'
* @code
* <pre>
* // 화살표 함수 표현식
* const greeting1 = ( name : { name : string, greeting : string } ) : void => { console.log( name.name + name.greeting ) }
* greeting1( { name : 3333 , greeting : " hello" } );
* </pre>
* */따라서 타입스크립트는 선택적/기본 매개변수를 선택할 수 있도록 만들었다.
타입스크립트는 매개변수를 선택적/기본적 매개변수로 지정할 수 있다. 선택적 매개변수를 사용하면 해당 매개변수를 전달하지 않도록 구현하기 위해서 사용되나 보통 자바스크립트에도 제공하는 기본 매개변수를 많이 사용하자.
나머지 매개변수는 가변 인자 함수를 만들기 위해서 사용된다.
제러레이터 함수는 여러개 값을 생성하는 함수이다. 쉽게 말하면 DB의 시퀀스와 같은 기능을 제공할 수 있다. 제너레이터는 소비자가 요청하는 순간에만 다음 값을 계산한다는 특징이 있다.
-
기본 매개변수
- 인수가
undefined로 들어올 시 기본 값으로 실행한다. - 선언 :
parameter = T
- 인수가
-
선택적 매개변수
- 옵션으로 넣을 수 있는 매개변수이다.
- 반드시 일반 매개변수 뒤에 와야한다.
- 당연히 선택적 매개변수가 앞에 오면 어떤 값이 필수인지 알 수 없기 때문
- 선언 :
parameter? : T
-
나머지 매개변수
- argument를 더 안전하게 사용하기 위해서 사용된다.
- 반드시 일반 매개변수 뒤에 와야한다
-
제너레이터 함수
-
여러개의 값을 생성할 수 있는 기능을 제공하는 함수
-
선언
// 0. 제너레이터 함수는 *을 붙여서 표현한다. 제너레이터를 호출하면 이터러블 반복자가 반환된다. function* generator() : IterableIterator<number>{ let firstNum : number = 0; let secondNum : number = 1; // 1. 소비자가 값을 호출하면 yield로 값을 호출한 후 소비자 요청 전까지 실행을 중지한다. while (true) { yield a; // 2. 제너레이터에서 yield 키워드로 값을 방출한다. [a, b] = [b, a + b] } } const fibonacci = generator(); fibonacci.next(); // { value : 0, done : false } fibonacci.next(); // { value : 1, done : false } fibonacci.next(); // { value : 2, done : false } fibonacci.next(); // { value : 3, done : false }
-
-
선택적 매개변수
// 선택적 매개변수 const showMessage = function ( message : string, userId? : string ) : void { const sender = userId ? userId : "익명"; console.log( "====================" ); console.log( message ); console.log( "====================" ); console.log( "- " + sender + " -" ); } showMessage( "옵션 값이 없습니다." ); showMessage( "옵션 값이 없습니다." , "김진표" );
==================== 옵션 값이 없습니다. ==================== -익명- ==================== 옵션 값이 없습니다. ==================== -김진표- -
나머지 매개변수
// 나머지 매개변수 const showMessageAll = function ( messageDisplay : Function , ...messages : string[] ) { messages.forEach( message => { messageDisplay( message ) }); }
지금까지 여러 타입과 그 타입을 이용한 함수의 매개변수와 반환 타입을 지정하는 방법을 보았다. 그렇다면 함수의 타입은 무엇일까?
함수는 Function 타입이다. 위 예제에서 Function 타입을 인수로 받는 함수가 하나 있었다.
// 나머지 매개변수
const showMessageAll = function ( messageDisplay : Function , ...messages : string[] ) {
messages.forEach( message => { messageDisplay( message ) });
}당연히 Object 타입 처럼 Function은 모든 함수를 받을 수 있는 타입이므로 별로 좋지 않는 타입이라고 할 수 있다.
여기서 호출 시그니처, 타입 시스니처가 필요하다. 호출 시그니처는 function의 입력과 출력을 정의한 것을 말한다. 함수 시그니처에는 타입 정보만 표현한다. 따라서 기본값은 표현할 수 없다. 또한 함수 바디가 없으므로 반환타입을 명시적으로 적어야 한다.
-
단축형 호출 시그니처
type fn = { parameter1 : T, parameter2? : T } => T
-
전체 호출 시그니처
type fn = { { parameter1 : T, parameter2? : T } : T }
이제 정리해보자, 함수를 타입으로 선언하기 위해서는 함수 시그니처를 선언해야 한다. 위 showMessageAll에서 messageDisplay의 Function 타입을 대신할 타입을 함수 시그니처로 타입을 만들어보자
type showMessage = ( message : string , userId? : string ) => void;
// 1. 일반적인 함수 선언
const showMessage : showMessage = function ( message : string, userId? : string ) : void {
const sender = userId ? userId : "익명";
console.log( "====================" );
console.log( message );
console.log( "====================" );
console.log( "- " + sender + " -" );
}
// 2. 선택적 파라메터와 기본값 파라메터는 서로 호환 됨
const showMessageWithDefault : showMessage = function ( message : string, userId = "익명") : void {
console.log( "====================" );
console.log( message );
console.log( "====================" );
console.log( "- " + userId + " -" );
}
// 파라메터 타입을 명시하지 않는 경우 1 : 문맥적 타입화
const showMessageWithContextual : showMessage = ( message , userId = "익명" ) => {
console.log( "====================" );
console.log( message );
console.log( "====================" );
console.log( "- " + userId + " -" );
}이제 showMessageAll 함수의 messageDisplay 타입을 변경해보자. 위 예제에서 다음의 경우를 유의하면 된다.
- showMessage에 선택적 매개변수가 있다는 것을 유의하라. 함수 시그니처를 구현한 함수는 선택적 매개변수를 기본값 매개변수로 대체할 수 있다.
- showMessageWithContextual에 파라메터 타입이 암시적으로 선언되었다는 것을 유의하라. 따로 설정 안하면 타입이 없으면 any로 추론하고 오류를 나타내지만(이 설정은 따로 변경할 수 있다. 기본은 안됨) 위 경우는 string, string으로 정상적으로 추론이 가능해서 사용이 가능한 경우이다. 이를 문맥적 타입화라고 한다.
const showMessageAllWithSigniture = function ( messageDisplay : showMessage , ...messages : string[] ) {
messages.forEach( message => { messageDisplay( message ) });
}showMessageAllWithSigniture( showMessage , "도쿄" , "런던" , "파리" );위 예제에서 showMessageWithContextual는 showMessage타입이므로 타입스크립트가 타입을 정상적으로 추론할 수 있는 상황이기 때문에 암시적으로 매개변수 타입을 선언한 경우이다. 이를 문맥적 타입화( Contextual typing )이라고 한다.
보통 일반적인 함수에서 암시적인 매개변수 타입 선언은 오류를 발생시킨다.
/**
* 파라메터를 암시적으로 선언할 수 없습니다.
*
* @error TS7006: Parameter 'exp' implicitly has an 'any' type.
* @code
* <pre>
* const exp = (exp) => exp;
* </pre>
* */
타입스크립트에서 전체 호출 시그니처를 사용해서 함수 오버로드를 구현할 수 있다.
오버로드된 함수
호출 시그니처가 여러개인 함수
타입스크립트는 오버로드된 함수 선언으로 동적 언어의 특징을 제공하고, 입력에 따라 달라지는 함수의 출력 타입은 정적 타입 시스템으로 구현한다.
위 예제에서 showMessage의 파라메터 중 userId는 선택적/기본값 파라메터를 사용해서 구현했다. 이를 함수 시그니처를 사용한 오버로드 기능으로 변경해보자
type showMessageOverload = {
( message : string ) : void
( message : string , userId : string ) : void
}이를 자바의 오버로드로 생각하고 그대로 생성하면 오류가 발생한다. 이는 타입스크립트의 시그니처 오버로딩을 처리하는 방식 때문에 발생한다. 함수 f에 여러 개의 오버로드 시그니처를 선언하면, 호출자 관점에서 f의 타입은 이들 오버로드 시그니처의 유니온이 된다. 즉 호출자 관점에서 본 위 타입의 타입은 ...
- ```typescript
type showMessageOverload = ( message : string, userIdOrVoid : string | void ) : void
이 조합된 시그니처 타입은 자동으로 추론되지 않으므로 f 구현시 직접 처리해야 한다. 이 부분을 주의해야 한다.
예를 들어 위 예제의 오버로딩을 구현하려면 다음과 같이 처리해야 한다.
const showMessageByUserid : showMessageOverload = function ( message : string, userIdOrVoid : string | void = "익명") : void {
// userId 있을 시 구현
if ( typeof userIdOrVoid === "undefined") {
showMessageWithContextual( message );
}
else {
showMessageWithContextual( message , userIdOrVoid );
}
// userId 없을 시 구현
}타입스크립트의 오버로드는 DOM API에서 유용하게 활용된다. 이 부분은 책을 확인해보자 . 타입스크립트에서 활용되는 오버로드의 예를 확인할 수 있다.
지금까지는 함수를 정의한 시점에 매개변수나 타입을 선언한 경우가 전부였다. 하지만 함수를 정의하는 시점에 타입을 선언하기 어려운 경우가 있다.
예를 들어 number, string, 객체 배열을 받을 수 있는 filter 함수의 시그니처를 만들어보자.
type FilterWithNotGeneric = {
(array : number[], f: (item:number) => boolean ) : number[] ,
(array : string[], f: (item:string) => boolean ) : string[] ,
(array : object[], f: (item:object) => boolean ) : object[] ,
}위 오버로드 예제처럼 위 함수를 구현하기 위해서는 저 3개 타입을 합칠 필요가 있다. 당연히 저 3개를 구현하기에는 어려움이 있다.
이때 제너릭이 사용된다. 제너릭은 함수나 클래스 내부가 아닌 외부 사용자에 의해 타입이 지정되는 것을 말한다. 따라서 선언 시점이 아닌 사용 시점에 타입이 지정된다.
제너릭을 사용해서 위 함수 시그니처를 구현해보자
type FillterWithGeneric = {
<T>( array : T[], f ( item : T ) => boolean ) : T[]
}이제 filter function을 구현해보자
const filterFunction : FilterWithGeneric = function ( array , func ) {
let result = [];
for ( let i = 0; i < array.length ; i++ ) {
const item = array[i];
if ( func(item) ) {
result.push(item);
}
}
return result;
}console.log( filterFunction( [ {name : "an"} , {name : "kim"} ] , _ => _.name === "an" ) );
console.log( filterFunction( [ "an" , "kim" ] , _ => _ === "an" ) );제너릭을 사용하면 사용 시점에 타입스크립트가 제너릭 T를 추론해서 타입을 제한한다. 아래 예제를 보면 array로 string[]을 넘겼기 때문에 타입스크립트는 f 콜백 함수의 T를 string으로 추론한다. 따라서 아래 예제는 오류가 발생한다.
// 제너릭은 제너릭 위치에 기초하여 적절한 구체 타입을 한정한다. 아래는 T[]를 string[]으로 한정하였으므로
// Property 'name' does not exist on type 'string' 오류가 발생한다.
// 타입스크립트는 array에 넘겨진 배열이 string[]임을 알았으므로 객체를 받는 함수에 오류를 발생시킨다.
console.log( filterFunction( [ "an" , "doc" ] , _ => _.name === "an" ) );제너릭이 타입을 한정하는 순간은 의 위치에 따라 달라진다. 위 예제에서 FilterWithGeneric는 호출 시그니처의 일부로 선언했으므로 타입스크립트는 FilterWithGeneric 타입 함수를 실제 호출할 시 구체 타입을 T로 한정한다.
이를 FilterWithGeneric 타입의 별칭으로 한정하려면 FilterWithGeneric 타입 선언 시 명시적으로 한정하게 해야한다.
아래 예제는 위 filter의 제너릭을 타입 별칭 사용시로 타입을 한정하도록 만든 예이다.
type FilterWithGenericTypeAlias<T>= {
( array : T[] , f : ( item : T ) => boolean ) : T[]
}
const filterFunctionWithGenericTypeAlise : FilterWithGenericTypeAlias<number> = function ( array , func ) {
//... 생략
}console.log( filterFunctionWithGenericTypeAlise( [ 1 , 2 , 3 , 4 , 5 ] , _ => _ >= 3) );
제너릭은 다음과 같이 선언할 수 있다.
-
단축형/전체 시그니쳐의 개별 시그니쳐에 선언 : 각각의 filter 호출은 자신만의 T 한정 값을 가진다.
// 단축형 type filter = <?> ( array : T[] , (item : T) => boolean ) => T[] // 전체 type FilterWithGenericTypeAlias= { <T>( array : T[] , f : ( item : T ) => boolean ) : T[] }
-
단축형/전체 시그니쳐의 개별 시그니쳐에 선언 : 모든 filter 타입 함수를 선언할 때 T를 한정한다.
// 단축형 type filter<T> = ( array : T[] , (item : T) => boolean ) => T[]; // 전체 type FilterWithGenericTypeAlias<T> = { ( array : T[] , f : ( item : T ) => boolean ) : T[] }
-
함수 정의에도 제너릭을 사용할 수 있다.
function filter<?> (array : T[], f : (item:T)=>boolean) { //... 구현 }
제너릭의 타입을 타입스크립트가 추론하게 할 수 있지만 이를 명시적으로 지정할 수 있다. 단 명시적으로 제너릭의 타입을 지정할 경우 모든 타입을 제너릭대로 명시하거나 아무것도 명시해서는 안된다.
// 함수 정의시에도 사용할 수 있다.
function map<T,U>( array : T[] , f : ( item : T ) => U ) : U[] {
let result = [];
for ( let i = 0; i < array.length ; i++ ) {
result[i] = f(array[i]);
}
return result;
}-
위 함수는 다음과 같이 사용할 수 있다.
// 제너릭 타입을 추론하게할 경우 const mapResult = map( [ 1 , 2 ,3 ] , _ => { return _ + 3 } ); // <number,number> // 제너릭 타입을 명시할 경우 const mapResult2 = map<number , string>( [ 1 , 2 ,3 ] , _ => { return _ + "3" } ) // <number,string>
-
특히 프로미스를 사용할 경우 then이나 catch에서 타입을 명시해야 한다.
new Promise<number>
제너릭을 사용하면 타입의 상속 형태를 구현할 수 있다. 제너릭의 범위를 제한하는 extends 키워드를 사용하면 된다.
형식은 다음과 같다 : 제너릭 T는 U의 제한을 받는다.
function fn<T extends U> ( param : T ) {
//... 구현
}아래 코드를 살퍄보자
function showId <T> ( user : T ) : void {
user.showMe();
}위 예제는 오류가 발생한다. 형식 T의 타입이 확정되어도 T에 showMe하는 메소드가 존재하는지 알 수 없기 때문이다. 따라서 extends 키워드를 사용한다.
function showId <T extends user> function( user : T ) {
user.showMe();
}아래는 실제 예제 코드이다.
type user = {
userId : string,
showMe : () => void
}
type staffUser = user & {
personalNo : string
}
type normalUser = user & {
googleId : string
}
const user1 : user = {
userId : "1",
showMe : () => {console.log( "나는 유저!" ); }
}
const user2 : staffUser = {
userId : "2",
personalNo: "",
showMe() { console.log( "나는 스테프 유저!" ); }
}
const user3 : normalUser = {
userId : "1",
googleId : "12",
showMe() { console.log( "나는 노말 유저!" ); }
}
showId( user1 );
showId( user2 );
showId( user3 );
// 덕타이핑 : 인식
showId( { userId : "해킹 유저", showMe : () => {console.log( "나는 오리" )} } )인터섹션 (&)을 사용해서 제한 수준을 여러 타입으로 늘릴 수 있다. 위 코드와 비교하라 showIdWithProperty는 프로퍼티 타입과 유저 타입이 모두 구현된 타입만 받을 수 있다.
type user = {
userId : string,
showMe : () => void,
}
type staffUser = user & {
personalNo : string
}
type normalUser = user & {
googleId : string
}
type property = {
properties : string[],
showProperty : ( item : string ) => void
}
const user1 : user & property = {
userId : "1",
showMe : () => {console.log( "나는 유저!" )},
properties: [ "서울" ],
showProperty : ( pro ) => { console.log( pro ) }
}
const user2 : staffUser & property = {
userId : "2",
personalNo: "",
showMe() { console.log( "나는 스테프 유저!" ) },
properties: [ "대구" ],
showProperty : ( pro ) => { console.log( pro ) }
}
const user3 : normalUser & property = {
userId : "1",
googleId : "12",
showMe() { console.log( "나는 스테프 유저!" ); },
properties: [ "부산" ],
showProperty : ( pro ) => { console.log( pro ) }
}
// user를 상속받는 모든 타입을 받을 수 았다.
function showId<T extends user > ( user : T ) : void {
user.showMe();
}
function showIdWithProperty <T extends user & property > ( user : T ) : void {
user.showMe();
user.showProperty( user.properties[0] );
}
showIdWithProperty( user1 );
showIdWithProperty( user2 );
showIdWithProperty( user3 );
// 덕타이핑
showId( { userId : "해킹 유저", showMe : () => {console.log( "나는 오리" )} } )