함수 선언Optional Parameter, Default ParameterRest Parameterscall, apply, bindthis의 타입제너레이터반복자호출 시그니처( 타입 시그니처)문맥적 타입화(Contextual typing)오버로드된 함수 타입오버로드 예시(새로운 HTML 요소를 만들 때 사용하는 createElement DOM API)전체 호출 시그니처의 다른 이용케이스(오버로딩말고, 함수의 프로퍼티 만드는데도 사용가능)다형성(Generic Type)제네릭 추가 방식Type Alias 에 제네릭 선언함수 시그니처에 제네릭 선언제네릭 타입 추론Generic Bound(제네릭 한정)Generic Default Value 설정타입 주도 개발
함수 선언
function add(a: number, b: number): number { return a+ b; } // 실무에서는 타입스크립트가 반환 타입을 추론하도록 하는게 보통. 타입스크립트가 해줄 수 있는 일을 // 개발자가 직접 할 필요가 없기 때문
// 이름을 붙인 함수 function greet(name: string) { return 'hello ' + name; } // 함수 표현식 let greet2 = function (name:string) { return 'hello ' + name; } // 화살표 함수 표현식 let greet3 = (name: string) => { return 'hello ' + name; } // 단축형 화살표 함수 표현식 let greet4 = (name: string) => 'hello ' + name; // 함수 생성자 -- 안전하지 않으므로 사용하지 않는 편이 좋음 let greet5 = new Function('name', 'return "hello " + name');
Optional Parameter, Default Parameter
// Optional Parameter. 일반 파라미터 의 뒤에 와야함 function log(message: string, userId? string) { let time = new Date().toLocaleTimeString(); console.log(time, message, userId || 'Not signed in'); } // Default parameter를 사용하면 ? 와 타입 을 지정할 필요가 없음 function log(message: string, userId = 'Not signed in') { let time = new Date().toLocaleTimeString(); console.log(time, message, userId); } // Default parameter에도 타입 지정이 가능 type Context = { appId? : string userId? : string } function log(message: string, context : Context = {}) { let time = new Date().toLocaleTimeString(); console.log(time, message, context.userId); }
- 실무에서는 Optional Parameter보다 Default Parameter를 더 자주 사용
Rest Parameters
- 가변인자 API가 필요할 때, 기존에는 자바스크립트 런타임이 함수에 자동으로 arguments를 정의해 개발자가 함수로 전달한 인수 목록을 할당하게 했음
- 그러나 arguments를 사용하는 것은 전혀 안전하지않음
function sumVariadic() : number { return Array.from(arguments).reduce((total, n) => total + n, 0); // parameter n 이 any로 추론이 되어 타입 안전하지 않음 } // 매개변수 목록 맨 마지막에 위치해야 함 function sumVariadicSafe(...numbers: number[]): number { return number.reduce((total, n) => total + n, 0); }
call, apply, bind
this의 타입
- this 변수가 클래스에 속한 메서드들 뿐 아니라, 모든 함수에서 정의된다는 사실이 특이하다
- this의 값은 함수를 어떻게 호출했는지에 따라 달라짐
따라서 많은 개발팀은 클래스 메서드를 제외한 다른 모든 곳에서 this 사용을 금함
TSLint 규칙에서 no-invalid-this를 활성화하면 코드에 이런 this가 침투하는 일을 방지할 수 있음
function fancyDate(this: Date) { return ${ this.getDate() } / ${this.getMonth()} /${ this.getFullYear() }; } fancyDate.call(new Date); fancyDate(); // 에러 TS2684: void 타입의 'this'를 //메서드에 속한 'Date' 타입의 'this'에 할당할 수 없음
TSC 플래그 : noImplicitThis
tsconfig.json에서 noImplicitThis를 활성화하면 함수에서 항상 this 타입을 명시적으로 설정하도록 강제할 수 있음
단, noImplicitThis는 클래스와 객체의 함수에는 this를 지정하라고 강제하지 않음
제너레이터
function* createFibonacciGenerator() { let a = 0; let b = 1; while (true) { yield a; [a, b] = [b, a + b]; } } let fibo = createFibonacciGenerator(); // IterableIterator<number> // IterableIterator에서 방출하는 타입을 감싸서 제너레이터의 타입을 명시할 수도 있음 function* createNumbers() : IterableIterator<number> { let n = 0; while (1) { yield n++; } }
반복자
- 반복자와 제너레이터는 상생관계임. 제너레이터로 값의 스트림을 생성할 수 있고 반복자로 생성된 값을 소비할 수 있기 때문에
- 이터러블(Iterable) : Symbol.iterator 라는 프로퍼티(반복자를 반환하는 함수)를 가진 모든 객체
- 반복자(iterator) : next라는 메서드(value, done 두 프로퍼티를 가진 객체를 반환)를 정의한 객체
TSC 플래그 : downlevelIteration
타입스크립트를 ES2015 이전 버전의 자바스크립트로 컴파일 할 때는 tsconfig.json에서 downlevelIteration 플래그로 커스텀 반복자를 활성화 할 수 있음
응용 프로그램의 번들 크기가 커지는 것을 원하지 않으면 downlevelIteration을 비활성화 하는 것이 좋음. 예전 환경에서 커스텀 반복자를 지원하려면 많은 코드가 필요하기 때문에
호출 시그니처( 타입 시그니처)
function sum(a: number, b: number): number { return a + b; } // call signature (a: number, b: number) => number
- sum 의 타입은? 함수이므로
Function
타입이라 말할 수 있음 - 그러나 이는
object
로 모든 객체를 가리킬 수 있는 것처럼Function
은 모든 함수의 타입을 뜻할 뿐이며 그것이 가리키는 특정 함수와 타입과 관련된 정보는 아무것도 알려주지 않음
- 이 문법은 화살표 함수와 아주 비슷한데, 이는 의도된 것임
- 함수 호출 시그니처는 타입 수준 코드, 즉 값이 아닌 타입 정보만 포함함
타입 수준 코드와 값 수준 코드
정적 타입 프로그래밍에서 사람들은 ‘타입 수준’, ‘값 수준’이라는 용어를 자주 사용하는데, 이들은 공용 어휘임
이 책에서 사용하는 타입 수준 코드는 타입과 타입 연산자를 포함하는 코드
값 수준 코드는 그 밖의 모든 것
type Log = (message: string, userId?: string) => void let log: Log = ( message, // 매개변수의 타입을 다시 지정할 필요가 없음 userId = 'Not signed in' // 호출 시그니처는 값을 포함할 수 없으므로 기본값은 여기서 지정 ) => { // 반환 타입도 다시 지정할 필요 없음 let time = new Date().toISOString(); console.log(time, message, userId); }
문맥적 타입화(Contextual typing)
- 위의 호출 시그니처 주어지고, 해당 시그니처를 만족하는 함수를 구현할 때 타입을 지정하지 않아도 타입을 알 수 있었음 ⇒ 이것이 문맥적 타입화
function times( f: (index: number) => void, n: number ) { for (let i = 0; i < n; i++) { f(i); } } times(n => console.log(n), 4); /* times 호출 시 함수 선언을 인라인으로 제공하면 인수로 전달하는 함수의 타입을 명시할 필요가 없음 위의 시그니처에서 f 의 인수 index를 number로 선언했으므로 타입스크립트는 문맥상 n이 number임을 추론할 수 있음 */ // f 를 인라인으로 선언하지 않으면 타입스크립트가 타입을 추론할 수 없음 function f(n) { console.log(n); } times(f, 4);
오버로드된 함수 타입
// 단축형 호출 시그니처 type Log = (message: string, userId?: string) => void // 전체 호출 시그니처 type Log = { (message: string, userId?: string): void }
- 대부분의 프로그래밍 언어에서는 여러 매개변수를 인수로 받아 어떤 타입의 값을 반환하는 함수를 선언한 다음, 이 함수가 요구하는 정확한 매개변수 집합을 건네 함수를 호출하면 항상 똑같은 타입의 반환값을 받게 됨
- 그러나 자바스크립트는 예외. 자바스크립트는 동적 언어이므로 어떤 함수를 호출하는 방법이 여러가지임. 뿐만 아니라 인수 입력 타입에 따라 반환 타입이 달라질 때도 있음
- 타입 스크립트는 이런 동적 특징을 오버로드된 함수 선언으로 제공하고, 입력 타입에 따라 달라지는 함수의 출력 타입은 정적 타입 시스템으로 각각 제공함
오버로드된 함수 : 호출 시그니처가 여러 개인 함수
type Reserve = { (from: Date, to: Date, destination: string): Reservation (from: Date, destination: string): Reservation } /* 해당 함수 표현식은 에러를 발생시킴. 그 이유는 이 함수 표현식이 위의 Reserve에 선언된 전체 호출 시그니처 모두를 만족시킬 수 있는 함수 표현식이 아니기 때문 */ let reserve: Reserve = (from, to, destination) => { // ... } /* 함수 표현식의 구현의 시그니처는, 두 개의 오버로드 시그니처를 수동으로 결합한 결과와 같음 두 가지 방식으로 reserve를 호출할 수 있으므로 reserve를 구현할 때 타입스크립트에 reserve가 어떤 방식으로 호출되는지 확인시켜 주어야 함( 6.1.5 정제 참고) */ let reserve: Reserve = ( from: Date, toOrDestination: Date | string, destination?: string ) => { if (toOrDestination instanceof Date && destination !== undefined) { // 편도 여행 예약 } else if (typeof toOrDestination === 'string') { // 왕복 여행 예약 } }
오버로드 시그니처는 구체적으로 유지하자
오버로드된 함수 타입을 선언할 때는 각 오버로드 시그니처(Reserve)를 구현의 시그니처(reserve)에 할당할 수 있어야 한다. 즉 오버로드를 할당할 수 있는 범위에서 구현의 시그니처를 얼마든지 일반화 할 수 있음 — 전부다 any로 해버릴 수 있는것
(그러나 그렇게 하면 함수 구현 시, 해당 타입이 내가 원하는 타입이 맞는지 증명하는 과정이 필욯게 되어, 가능한 구체적으로 유지하는 것이 좋다는 말)
let reserve: Reserve = ( from: any, toOrDestination: any, destination?: any ) => { // ... } let reserve: Reserve = ( from: Date, toOrDestination: Date | string, destination?: string ) => { // ... }
오버로드 예시(새로운 HTML 요소를 만들 때 사용하는 createElement DOM API)
type CreateElement = { (tag: 'a'): HTMLAnchorElement (tag: 'canvas'): HTMLCanvasElement (tag: 'table'): HTMLTableElement (tag: string): HTMLElement } /* 구현의 매개변수는 createElement의 오버로드 시그니처가 가질 수 있는 모든 매개변수 타입을 합친 타입을 지원해야 함. 세 개의 문자열 리터럴 타입은 모두 string의 서브타입이므로 간단하게 타입 유니온 결과를 string으로 축약할 수 있음 */ let createElement: CreateElement = (tag: string): HTMLElement => { // ... }
function createElement(tag: 'a'): HTMLAnchorElement function createElement(tag: 'canvas'): HTMLCanvasElement function createElement(tag: 'table'): HTMLTableElement function createElement(tag: string): HTMLElement { // ... }
전체 호출 시그니처의 다른 이용케이스(오버로딩말고, 함수의 프로퍼티 만드는데도 사용가능)
다형성(Generic Type)
type Filter = { (array: number[], f: (item: number) => boolean): number[] (array: string[], f: (item:string) => boolean): string[] (array: object[], f: (item: object) => boolean): object[] } let names = [ { firstName: 'beth' }, { firstName: 'caitlyn' }, { firstName: 'xin' } }; // 에러 TS2339: 'firstName' 프로퍼티는 'object' 타입에 존재하지 않음 let result = filter(names, _ => _.firstName.startsWith('b')); result[0].firstName; // 'firstName' 프로퍼티는 'object' 타입에 존재하지 않음 // object는 객체라는 것만 알고 세부 사항들은 알려주지 못함 //이럴 때 제너릭을 이용하면 됨. type Filter = { <T>(array: T[], f: (item: T) => boolean): T[] }; let filter: Filter = (array, f) => // ... filter([1,2,3], _ => _ > 2); filter(['a', 'b'], _ => _ !== 'b');
- 보통 타입스크립트는 제네릭 타입을 사용하는 순간에 제네릭과 구체 타입을 한정함(bind)
- 제네릭을 사용할 때
- 함수에서는 함수를 호출할 때
- 클래스라면 클래스를 인스턴스화 할 때
- 타입 별칭과 인터페이스에서는 이들을 사용하거나 구현할 때
제네릭 추가 방식
Type Alias 에 제네릭 선언
// T의 범위를 모든 시그니처로 한정한 전체 호출 시그니처. T를 Filter 타입의 일부로(특정 시그니처 타입x) // 선언했으므로 타입스크립트는 Filter 타입의 함수를 선언할 때 T를 한정함 type Filter<T> = { (array: T[], f: (item: T) => boolean): T[] } // 위와 비슷한 단축 호출 시그니처 type Filter<T> = (array: T[], f: (item: T) => boolean) => T[]
- 타입스크립트가 Filter 타입의 함수를 실제 호출할 때 구체 타입을 T 로 한정
함수 시그니처에 제네릭 선언
// T를 시그니처 범위로 한정한, 이름을 갖는 함수 호출 시그니처 function filter<T>(array: T[], f: (item: T) => boolean): T[] { // ... } // 아래와 비슷한 단축 호출 시그니처 type Filter = <T>(array:T[], f:(item: T) => boolean) => T[] // T의 범위를 개별 시그니처로 한정한 전체 호출 시그니처 type Filter = { <T>(array: T[], f: (item: T) => boolean): T[] }
제네릭 타입 추론
- 명시적으로 지정해주거나, 타입스크립트가 추론하도록 아무것도 명시하지 않거나
function map<T, U>(array: T[], f: (item: T) => U): U[] { // ... } map(['a', 'b', 'c'], _=> _ === 'a'); map<string, boolean>(['a', 'b', 'c'], _ => _ === 'a');
let promise = new Promise(resolve => resolve(45)); promise.then(result => result * 4); // {} 로 추론함 // 에러 TS2362: 수학 연산의 왼쪽 연산자는 'any', 'number', 'bigint', enum 타입 중 하나여야 함 let promise = new Promise<number>(resolve => resolve(45));
Generic Bound(제네릭 한정)
type TreeNode = { value: string } type LeafNode = TreeNode & { isLeaf: true } type InnerNode = TreeNode & { children: [TreeNode] | [TreeNode, TreeNode] } let a: TreeNode = {value: 'a'} let b: LeafNode = {value: 'b', isLeaf: true} let c: InnerNode = {value: 'c', children: [b]} let a1 = mapNode(a, _ => _.toUpperCase()); // TreeNode let b1 = mapNode(b, _ => _.toUpperCase()); // LeafNode let c1 = mapNode(c, _ => _.toUpperCase()); // InnerNode function mapNode<T extends TreeNode>( node: T, f: (value: string) => string ): T { return { ...node, value: f(node.value); } }
- T가 TreeNode를
extends
하지 않으면 node.value가 에러를 발생시킴
- T를 아예 사용하지 않고
node: TreeNode
를 하게 되면 매핑되면서 타입 정보가 날아가서 a1, b1, c1 모두 TreeNode가 됨
Generic Default Value 설정
type MyEvent<T = HTMLElement> = { target: T type: string }
타입 주도 개발
타입 주도 개발(type-driven development)
타입 시그니처를 먼저 정하고 값을 나중에 채우는 프로그래밍 방식
- 타입스크립트 프로그램을 구현할 때는 먼저 함수의 타입 시그니처를 정의한 다음 구현을 추가함
- 구현을 시작하기 전에 프로그램을 타입 수준에서 구상해보면 모든 것이 이치에 맞는지를 상위 수준에서 확인할 수 있다.