-
01-타입정의하기Programming/Typescript 2021. 11. 22. 18:41반응형
인프런 이재승 님 타입스크립트 시작하기 강의를 듣고 여러 레퍼런스를 참고하여 정리한 노트입니다.
1. 몇 가지 기본 타입1
const v: number = 123; const tf: boolean = 123 > 343; const msg: string = '하이'; const list: number[] = [1, 2, 3]; const list2: Array<number> = [1, 2, 3]; list.push('a'); // list는 number만 들어갈 수 있기 때문에 에러 발생 // tuple: 배열처럼 생겼지만 각 인덱스별로 타입을 미리 지정해놓음 const tuple: [string, number] = ['123', 123];자바스크립트에서는 array라는 타입은 없고 object만 있다.
즉, 타입스크립트를 사용하면 보다 상세하게 타입을 관리할 수 있게 된다.
참고
2. 몇 가지 기본 타입2
undefined와 null
undefined와 null도 타입 지정이 가능하다
// 현재 v1은 undefined만 가능하다. let v1: undefined = undefined; let v2: null = null; v1 = 123; // 보통 undefined와 null은 다른 타입과 같이 사용한다. // | 는 유니온 타입으로, or다. // v3는 number와 undefined를 지정할 수 있다. let v3: number | undefined = undefined; v3 = 123;자바스크립트에서 undefined는 undefined라는 타입으로 지정이 되어 있지만, null은 object로 되어 있다.
이점은 자바스크립트의 명백한 버그로 예기치 못한 사이드 이펙트를 야기하기도 한다.
리터럴 타입 지정
또 타입스크립트의 재밌는 점은 어떤 특정 숫자와 문자열 리터럴도 타입으로 지정이 가능하다는 점이다.
let n1: 10 | 20 | 30; n1 = 10; n2 = 15; // error let str: '백' | '프론트'; str = '프론트'; str = '미들'; // errorany 타입
any 타입은 모든 타입을 지정할 수 있다.
JS -> TS로 마이그레이션 하는 경우에 유용하게 쓰일 수 있다.
타입에러가 나는 부분을 any로 설정해두고 차근히 타이핑 해나가면 된다.
타입을 알 수 없는 경우, 타입 정의가 안된 외부 패키지를 사용하는 경우에 사용한다.
any를 남발하면 TS를 사용하는 의미가 퇴색되기 때문에 남발하지 않도록 한다.
let some: any; some = 123; some = "123"; some = () => { };함수의 반환 타입
// void: 아무것도 반환하지 않고 함수가 종료될 때. function f1(): void { console.log('hello'); } // never: 항상 예외가 발생하여 비정상적으로 함수가 종료될 때 function f2(): never { throw new Error('some error'); } // never: 무한루프 때문에 종료되지 않는 함수 function f3(): never { while (true) { // TODO } } function f4(): string { return '123'; } function f5(): string { return 123; // error }never는 거의 사용되지 않음
object 정의
let obj: object; obj = { name: 'abc' }; console.log(obj.name) // 속성 타입 정보가 입력되지 않아서 접근할 수 없음 // 속성정보를 포함해서 object의 타입 정보를 입력하려면 interface를 써야 함유니온( | )과 인터섹션( & )
let foo: (1 | 3 | 5) & (3 | 5 | 7); foo = 3; foo = 5; foo = 1; // error foo = 7; // error유니온은 합집합이고, 인터섹션은 교집합이다.
type 키워드를 이용하여 타입에 별칭 넣기
type Width = number | string; let wid: Width; wid = 100; wid = '100'; wid = true; // error3. enum 타입
자바스크립트에는 없고 TS에만 있다
number를 할당할 때
enum Fruit { Apple, Banana, Orange } const v1: Fruit = Fruit.Apple; // 값으로 할당 가능 const v2: Fruit.Apple | Fruit.Banana = Fruit.Banana; // 타입으로 지정 가능프룻과 그 안에 있는 건 타입과 값으로 이용 가능하다.
위 코드를 js로 컴파일한 결과는 아래와 같다.
"use strict"; var Fruit; (function (Fruit) { Fruit[Fruit["Apple"] = 0] = "Apple"; Fruit[Fruit["Banana"] = 1] = "Banana"; Fruit[Fruit["Orange"] = 2] = "Orange"; })(Fruit || (Fruit = {})); const v1 = Fruit.Apple; const v2 = Fruit.Banana;enum에 숫자를 값으로 입력할 경우에는 양방향 맵핑이 된다.
위와 같이 Apple, Banana, Orange에 아무런 값도 설정하지 않으면 차례료 0부터 할당이 되고, 할당되지 않은 값은 이전 값의 +1되어 할당된다.
따라서
enum Fruit { Apple, // 0 Banana = 5, Orange // 6 } const v1: Fruit = Fruit.Apple; const v2: Fruit.Apple | Fruit.Banana = Fruit.Banana; console.log(Fruit.Banana); // 5 console.log(Fruit['Banana']); // 5 console.log(Fruit[5]); // "Banana"와 같다.
string을 할당할 때
enum은 숫자뿐만 아니라 문자열도 할당할 수 있다.
enum Language { Korean = 'ko', English = 'en', Japanese = 'jp' }"use strict"; var Language; (function (Language) { Language["Korean"] = "ko"; Language["English"] = "en"; Language["Japanese"] = "jp"; })(Language || (Language = {}));enum에 문자열을 할당하는 경우에는 단방향으로만 맵핑이 된다.
enum Language { Korean = 'ko', English = 'en', Japanese = 'jp' } console.log(Language.Korean); // 'ko' console.log(Language['Korean']); // 'ko' console.log(Language['ko']); // error응용
위와 같은 enum 객체의 특성을 이해했다면 아래와 같은 유틸함수를 만들 수 있다.
enum Language { Korean = 'ko', English = 'en', Japanese = 'jp' } const getIsValidEnumValue = (enumObject: any, value: number | string) => { return Object.keys(enumObject) .filter(key => isNaN(Number(key))) // * enum객체의 양방향 맵핑을 고려하여 숫자가 key일 경우를 제외 시킴 .some(key => enumObject[key] === value); }; // * // enum Some { // Key1 = 1 // } // Some['Key1'] === 1; // Some[1] === 'Key1'; // Some[1] === 'Key1'; 이 경우를 제외시킨다.이 함수를 사용해보면
enum Fruit { Apple, Banana, Orange } enum Language { Korean = 'ko', English = 'en', Japanese = 'jp' } const getIsValidEnumValue = (enumObject: any, value: number | string) => { return Object.keys(enumObject) .filter(key => isNaN(Number(key))) // * enum객체의 양방향 맵핑을 고려하여 숫자가 key일 경우를 제외 시킴 .some(key => enumObject[key] === value); }; console.log('1 in Fruit:', getIsValidEnumValue(Fruit, 1)); // true console.log('5 in Fruit:', getIsValidEnumValue(Fruit, 5)); // false console.log('Orange in Fruit:', getIsValidEnumValue(Fruit, 'Orange')); // false console.log('ko in Language:', getIsValidEnumValue(Language, 'ko')); // true console.log('Korean in Language:', getIsValidEnumValue(Language, 'Korean')); // false위와 같이 값이 있으면 true, 값이 없으면 혹은 값이 아니라 아이템의 이름을 적은 경우는 false가 된다.
const enum: 번들파일 최적화
enum을 사용하면 컴파일 후에도 enum 객체가 남아있기 때문에 번들파일이 크기가 커지게 된다.
위와 같이 enum에 직접 접근하는 경우가 없으면 enum을 컴파일 후에 제거해줘야 하는데, 이때 사용되는 게
const enum이다.const enum Fruit { Apple, Banana, Orange } const fruit: Fruit = Fruit.Apple; const enum Language { Korean = 'ko', English = 'en', Japanese = 'jp' } const lang: Language = Language.Korean;위와 같이 작성하면 컴파일 후에는
"use strict"; const fruit = 0 /* Apple */; const lang = "ko" /* Korean */;이렇게만 남고 enum의 객체는 사라진다.
물론 이 경우에는
getIsValidEnumValue(Fruit, 1) // error: 'const' enums can only be used in property or index access expressions or the right hand side of an import declaration or export assignment or type query.(2475)위와 같이 함수를 사용할 수 없지만 다행히 IDE가 에러를 알려준다.
이렇게 const enum을 사용하면 번들 파일의 크기를 줄일 수 있다.
참고
- enum을 사용하는 이유: https://medium.com/@seungha_kim_IT/typescript-enum%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0-3b3ccd8e5552
- enum을 자제해야 하는 이유: https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking/
4. 함수 타입1
함수에 타입을 지정하는 방법을 알아보자
// 매개변수와 리턴값의 타입을 지정해줄 수 있다. function getText(name: string, age: number): string { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}`; } const getText2 = (name: string, age: number): string => { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}`; }위 코드에서 getText 함수의 파라미터 중 name은 string, age는 number로 타입 지정이 되었고 반환값은 string으로 타입지정 되었다.
const v1: string = getText('mike', 23); const v2: string = getText('mike', '23'); // error: age는 number로 지정되었다. const v3: number = getText('mike', 23); // error: getText의 반환값은 string이다.위와 같은 코드를 작성하면 에러가 발생한다.
함수를 저장하는 변수의 타입은 아래처럼도 지정할 수 있다.
const getText3: (name: string, age: number) => string = function (name, age) { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}`; }함수를 표현하는 지점에는 따로 타입을 지정하지 않아도 된다.
getText2와 컴파일된 결과를 비교해보면 아래와 같다.
// getText2 const getText2 = (name, age) => { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}`; }; // getText3 const getText3 = function (name, age) { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}`; };getText2는 화살표 함수로 표현되었고, getText는 function 키워드로 정의된 함수를 담은 함수 표현식이 되었다.
optional parameter(?:) - 선택 매개변수
?:기호를 이용해서 optional parameter를 사용할 수 있다.// language는 optional parameter로서 string이거나 undefined다. function getText(name: string, age: number, language?: string): string { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; const languageText = language ? language.substr(0, 10) : ''; return `name: ${nameText}, age: ${ageText}, language: ${languageText}`; }이때 language는 string이거나 undefined만 가능하다.
getText('simi', 23, 'ko'); getText('simi', 23); getText('simi', 23, 123); // error따라서 위 코드에서 language 자리에 123이 들어간 코드는 error가 된다.
optional parameter가 중간에 오는 경우
이때 optional parameter가 중간 순서에 오면 어떨까?
function getText(name: string, language?: string, age: number): string { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; const languageText = language ? language.substr(0, 10) : ''; return `name: ${nameText}, age: ${ageText}, language: ${languageText}`; }필수 파라미터는 선택적 파라미터 뒤에 올 수 없다. (error: A required parameter cannot follow an optional parameter.)
따라서 굳이 위 순서를 지키고 싶다면 아래와 같이 작성하고 사용한다.
function getText(name: string, language: string | undefined, age: number): string { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; const languageText = language ? language.substr(0, 10) : ''; return `name: ${nameText}, age: ${ageText}, language: ${languageText}`; } getText('simi', undefined ,23);하지만 위 방법은 사용성, 가독성이 모두 떨어지므로 사용하지 않는 것이 좋고
매개변수가 많은 경우에는 비구조화 문법을 이용하여 named parameter를 이용하는 것이 좋다.
파라미터 기본값 설정
파라미터 기본값을 설정하면 파라미터가 자동으로 optional parameter가 되고 타입 지정 또한 된다.
function getText(name: string, age: number = 15, language = 'korean'): string { return `name: ${name}, age: ${age}, language: ${language}`; } console.log(getText('simi',)); console.log(getText('simi', 23)); console.log(getText('simi', 36, 'english')); console.log(getText('simi', 36, 123)); // error: language는 string으로 타입 지정되었다.위 코드에서 age와 language에는 기본값이 설정되어 있기 때문에 자동으로 optional parameter가 된다.
따라서 해당하는 전달인자가 없더라도 에러가 나지 않는다.
또한 language에 할당된 기본값이 string이므로 자동으로 타입이 string이 되어 number값을 인자로 넘겨주면 error가 발생한다.
rest parameter
function getText(name: string, ...rest: number[]): string { return ''; } console.log(getText('simi', 1, 2, 3)); console.log(getText('simi', 1, '2', 3)); // error위와 같이 rest parameter도 사용 가능하다.
rest parameter는 명시적으로 지정된 파라미터(여기선 name)외에
전달된 모든 인자의 배열으로서 타입 지정은 항상 배열로 되어야 한다.
현재 위 코드에서는 number[]로 지정되었기 때문에 인자 중에 number가 아닌 것이 들어오면 error가 발생한다.
참고
- argument와 parameter의 차이 : http://taewan.kim/tip/argument_parameter/
5. 자바스크립트 this 이해하기
타입스크립트에서 this를 타입지정하는 법을 알기 전에
자바스크립트에서의 this를 먼저 이해하자
화살표 함수의 this
function Counter() { // 여기서 this는 counter라는 인스턴스 this.value = 0; this.add = amount => { this.value += amount; }; } const counter = new Counter(); console.log(counter) // value, add를 프로퍼티롤 갖고 있음 console.log(counter.value); // 0 counter.add(5); console.log(counter.value); // 5 const add = counter.add; add(5); console.log(counter.value); // 10new를 이용해서 instance 생성해서 호출하면Counter 내부에 정의된 this는 Counter의 instance를 가리키게 된다(counter)
화살표함수의
this는 해당 화살표 함수가 생성될 당시 상위 스코프의 함수나 클래스의 this를 가리킨다.위 코드에선 화살표 함수가 생성될 당시 상위 스코프의 함수의 this가 counter라는 instance이므로
add의 this는 counter가 된다.
일반함수의 this
function Counter2() { this.value = 0; // 일반함수로 정의 this.add = function (amount) { this.value += amount; } } const counter2 = new Counter2(); console.log(counter2.value); // 0 counter2.add(5); // 여기서 add함수를 호출한 주체는 counter2 console.log(counter2.value); // 5 const add2 = counter2.add; add2(5); // 여기서 함수를 호출한 주체는 전역객체(브라우저 : window = node.js : global) console.log(counter2.value);일반함수의 this는 해당 함수를 호출한 주체를 가리킨다.
counter2.add(5);가 호출될 때는 주체가counter2이지만add2가 호출될 때 this가 가리키는 게 counter2가 아니라 전역객체다.따라서 화살표 함수의 this는 생성될 당시의 this로 고정되기 때문에 정적이라고 볼 수 있고
일반함수의 this는 호출될 당시의 환경에 따라 달라지기 때문에 동적이라고 볼 수 있다.
클래스에서
class Counter3 { value = 0; // 일반함수 add(amount) { this.value += amount; } // 화살표 함수 add2 = (amount) => { this.value += amount; } }클래스에서도 마찬가지다.
일반함수일 때는 this가 동적으로 정의되고
화살표 함수일 때는 this가 항상 Counter3의 객체로 고정
일반 객체일 때
const counter3 = { value: 0, add: function (amount) { this.value += amount; } } console.log(counter3.value); // 0 counter3.add(5); // 일반함수 add가 호출될 당시 주체는 counter3. this -> count3 console.log(counter3.value); // 5 const add3 = counter3.add; add3(5); // 일반함수 add3가 호출될 당시 주체는 전역객체. this -> 전역객체 console.log(counter3.value); // 5const counter3 = { value: 0, add: (amount) => { // 여기서 this는 화살표함수를 감싸고 있는 일반 함수가 없기 때문에 항상 전역객체 this.value += amount; } } console.log(counter3.value); // 0 counter3.add(5); console.log(counter3.value); // 0 const add3 = counter3.add; add3(5); console.log(counter3.value); // 0정리
- 일반함수
- 해당 함수를 호출하는 주체에 따라서 this가 달라진다.
- 화살표함수
- 기본적으로는 this 바인딩 없이 전역격체이지만, 해당 함수가 생성될 당시 상위 스코프의 함수, 클래스의 this로 고정 바인딩 된다.
참고
6. 함수 타입2
this 타입 지정
타입스크립트에서 this의 타입을 어떻게 지정할 수 있는지 알아보자
// error function getParam(index: number): string { const params = this.split(','); if (index < 0 || params.length <= index) { return ''; } return this.split(',')[index]; }위와 같이 this의 타입을 별도로 지정해주지 않으면 에러가 발생한다.
function getParam(this: string, index: number): string { const params = this.split(','); if (index < 0 || params.length <= index) { return ''; } return this.split(',')[index]; }타입스크립트에서 맨 첫 번째 매개변수를 this로 설정하고 타입지정을 해주면
this 다음에 정의된 것들을 매개변수로 갖게 된다. 즉 여기서 index는 첫 번째 매개변수다.
prototype과 타입지정
자바스크립트에 내장된 타입에 기능을 주입하고 싶을 때는 prototype 을 이용해서 주입할 수 있는데
function getParam(this: string, index: number): string { const params = this.split(','); if (index < 0 || params.length <= index) { return ''; } return this.split(',')[index]; } // error: Property 'getParam' does not exist on type 'String' String.prototype.getParam = getParam; console.log('asdf, 1234, ok '.getParam(1));위의 코드는 getParam의 타입을 지정하지 않아 에러가 나고 있다.
이렇게 내장된 타입에 다른 속성을 주입하고 싶을 때는 interface를 이용하면 된다.
function getParam(this: string, index: number): string { const params = this.split(','); if (index < 0 || params.length <= index) { return ''; } return this.split(',')[index]; } interface String { getParam(this:string, index: number): string } String.prototype.getParam = getParam; console.log('asdf, 1234, ok '.getParam(1)); // '1234'아래는 또다른 예시
interface Object { getShortKeys(this: object): string[]; } Object.prototype.getShortKeys = function () { return Object.keys(this).filter(key => key.length <= 3); }; const obj = { a: 1, bb: 2, ccc: 3, dddd: 4 }; console.log(obj.getShortKeys()); // ['a', 'bb', 'ccc']오버로드(Overload)
add라는 함수를 아래 요구사항에 맞게 작성해보자
- 두 매개변수가 모두 문자열이면 문자열을 반환한다.
- 두 매개변수가 모두 숫자이면 숫자를 반환한다.
- 두 매개변수를 서로 다른 타입으로 입력하면 안 된다.
먼저 아래와 같이 짠다고 가정해보자
// 1. function add(x: number | string, y: number | string): number | string { if (typeof x === 'number' && typeof y === 'number') { return x + y; } else { const result = Number(x) + Number(y); return result.toString(); } } // add는 string 혹은 number를 반환하는데, v1은 number만 되기 때문에 error const v1: number = add(1, 2); console.log(add(1, '2')); // 이거 호출되면 안되는데 잘 호출되고 있음위 코드의 문제는 v1에서 error가 발생하고 있다는 점과 매개변수가 서로 다른 타입인데도 호출이 되고 있다는 점이다.
이 문제는 함수 오버로드를 사용하면 해결할 수 있다.
// 2. function add(x: number, y: number): number; function add(x: string, y: string): string; // 여기 위 두 라인은 단순한 타입 정보이기 때문에 컴파일 후 자바스크립트 코드에는 남지 않는다. function add(x: number | string, y: number | string): number | string { if (typeof x === 'number' && typeof y === 'number') { return x + y; } else { const result = Number(x) + Number(y); return result.toString(); } } // 이번엔 전달인자가 모두 number일 경우 반환값이 number로 보장되기 때문에 에러가 발생하지 않음 const v1: number = add(1, 2); console.log(add(1, '2')); // 오버로드한 타입 정보 중 이렇게 매개변수의 타입이 다른 경우가 없기 때문에 호출되지 않고 에러 발샘오버로드에 쓰인 타입 지정은 컴파일 후 사라져 자바스크립트 파일에 남지 않는다.
named parameter
다음은 객체 비구조화 문법을 사용한 named parameter에 타입을 지정하는 방법이다.
function getText({ name, age = 15, language }: { name: string; age?: number; language?: string; }): string { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}, language: ${language}`; } getText({ name: 'aaa', age: 11, language: '' }); getText({ name: 'aaa' }); // age와 language는 optional parameter이기 때문에 생략 가능만약 이때 위 타입 정보를 재사용하고 싶다면 interface를 사용하면 된다.
interface Param { name: string; age?: number, language?: string; } function getText({ name, age = 15, language }: Param): string { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}, language: ${language}`; }위와 같이 함수를 작성하다가 매개변수가 많아진다 싶으면 named parameter를 사용하는 게 좋다. (가독성을 위해)
팁
function getText(name: string, age = 15, language?: string): string { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}, language: ${language}`; }원래 위와 같이 쓰여있던 코드를 객체로 감싸고, 타입 지정을 다른 곳에 작성하는 등 번거로운 작업이 될 수 있다.
VS CODE에서는 해당 함수의 선언부(function 키워드부터 반환값 타입지정까지)에 커서를 두면
노란 전구 버튼이 생기는데, 이걸 클릭하면
Convert parameters to destructured object라는 버튼이 나오고 이걸 클릭하면 자동으로 변환해준다.오버로드 vs 오버라이딩 간단 정의
- 오버로드: 매개변수의 개수 또는 자료형에 따라서 함수를 다르게 선언
- 오버라이딩: 자식 클래스가 부모 클래스의 메소드를 재정의하는 것을 의미합니다.
참고
7. 인터페이스
자바와 같은 다른 언어에서 인터페이스는 클래스를 구현하기 전에 필요한 메서드를 정의하는 용도로 쓰인다.
TS에서는 좀더 다양한 것들을 정의하는 데 쓰인다.
TS에서 인터페이스로 정의할 수 있는 타입의 종류와 인터페이스로 타입을 정의하는 방법을 알아보자.
객체의 타입 지정
// interface 키워드 이용 // 타입의 이름(Person). // 대괄호 안에 필요한 속성을 입력. interface Person { name: string; age: number; } const p1: Person = { name: 'mike', age: 23 }; const p2: Person = { name: 'mike', age: '23' }; // age는 number로 지정되어 있기 때문에 error.선택 속성(optional property) 지정
interface Person { name: string; age?: number; // ?: 기호를 이용 } const p1: Person = { name: 'mike', age: 23 }; const p2: Person = { name: 'mike' }; // age는 선택 속성이기 때문에 가능함.?:기호를 이용하여 age를 선택 속성으로 만들었기 때문에 age를 정의하지 않아도 에러가 발생하지 않는다.interface Person { name: string; age: number | undefined } const p1: Person = { name: 'mike', age: 23 }; const p2: Person = { name: 'mike'}; // error const p3: Person = { name: 'mike', age: undefined}; // 사용 가능위의 경우는 선택속성과 다르다. age는 필수 속성이고, number 혹은 undefined가 들어와야 하는 상태다.
따라서 age를 정의하지 않으면 error가 발생한다.
readonly
아래는 readonly 속성의 사용예이다.
interface Person { readonly name: string; age: number | undefined } const p1: Person = { name: 'mike', age: 23 }; p1.name = 'john'; // errorreadonly 속성은 말 그대로 읽기 전용이기 때문에 속성값을 수정하려고 하면 error가 발생한다.
타입호환성?
타입호환성에 대해 잠깐만 살펴보고 가자
interface Person { readonly name: string; age?: number; } const p1: Person = { name: 'mike', birthday: '1993-05-03' }; // error const p2 = { name: 'simi', birthday: '1993-05-03' }; const p3: Person = p2; // 사용가능위 코드에서 Person에는 birthday가 정의되어 있지 않기 때문에
p1에서 birthday라는 속성을 정의하려고 하면 error가 발생한다.
그런데 별도의 타입이 없고, birthday를 속성으로 갖고 있는 객체 p2를
Person 타입으로 지정된 p3에 할당하면 에러가 발생하지 않는다.
이는 p3의 타입이 p2의 타입을 포함하는 더 큰 타입이기 때문이다.
인덱스 타입(index type)
interface Person { readonly name: string; age: number; [key: string]: string | number; // index type } const p1: Person = { name: 'mike', birthday: '1993-05-03', // 사용가능 age: '25', // error. };위 코드를 보면 좀 특이하게 타입이 지정되어 있다.
이를 index type이라고 한다. 여기서 key는 아무거나 해도된다. (예: [adsfg: string]: string | number 이렇게 정의해도 무방)
속성 이름이 문자열인 속성은 string 혹은 number라고 동적으로 처리하고 있는 타입이다.
따라서 p1에 정의된 birthday는 이 index type에 의해 타입이 정의되기 때문에 사용이 가능하다.
하지만 똑같이 속성의 key가 문자열인 age는 error가 발생하는데
age는 index type이 아니라 명시적으로 정의된
age: number에 정의되기 때문이다.index type 타입 호환성
// good interface YearPricaMap { [year: number]: number; [year: string]: string | number; } // bad interface YearPricaMap { [year: number]: number; // error [year: string]: string; }자바스크립트에서는 속성 이름에 숫자와 문자열을 사용할 수 있지만
속성 이름에 숫자를 사용하면 내부적으로 문자열로 변환해서 사용한다.
따라서 타입스크립트에선 숫자인 속성 이름의 값이 문자열인 속성 이름의 값으로 할당 가능한지 검사한다.
쉽게 말하면, 문자열인 key의 value 타입의 범위가 숫자인 key의 value의 타입 범위보다 넓어야 한다.
interface YearPricaMap { [year: number]: number; [year: string]: string | number; } const yearMap: YearPricaMap = {}; yearMap[1998] = 1000; // 가능 yearMap[1998] = 'abc'; // error yearMap['2000'] = 1000; // 가능 yearMap['1000'] = '1000'; // error yearMap['ten'] = '1000'; // key가 문자열일 때는 number, string 모두 가능위 interface를 이용한 코드를 보면
key가 number일 때는 number만 value로 할당 가능하지만
key가 string일 때는 number와 string 모두 할당 할 수 있다.
단, 인덱스로 문자열을 입력 하더라도 숫자로 파싱 가능하면 숫자로 인식하기 때문에 error가 발생한다.
함수 타입 지정
interface로 함수의 타입도 지정 가능하다.
interface GetText { // (parameter 타입 지정): return값 타입 지정 (name: string, age: number): string; } const getText: GetText = function (name, age) { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}`; }사실 위 코드는
type GetText = (name: string, age: number) => string; const getText: GetText = function (name, age) { const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}`; }이것과 동일하다.
함수의 속성값을 이용한 코드
자바스크립트에서는 함수도 속성값을 가질 수 있다. 따라서 아래와 같은 타입스크립트 코드가 가능하다.
interface GetText { (name: string, age: number): string; totalCall?: number; } const getText: GetText = function (name, age) { if (getText.totalCall !== undefined) { getText.totalCall += 1; console.log(`totalCall: ${getText.totalCall}`); } const nameText = name.substr(0, 10); const ageText = age >= 35 ? 'senior' : 'junior'; return `name: ${nameText}, age: ${ageText}`; }; getText.totalCall = 0; getText('', 0); // 'totalCall: 1' getText('', 0); // 'totalCall: 2'interface로 함수의 타입을 지정할 때 함수의 속성값 또한 타입 지정을 해줄 수 있다.
class로 구현하기
Java와 같이 타입스크립트의 interface 또한 Class로 구현이 가능하다.
interface Person { name: string; age: number; getIsYoungerThan(age: number): boolean; } class SomePerson implements Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } getIsYoungerThan(age: number) { return this.age < age; } }위 코드는 interface에서 정의한 타입들을 implements를 이용해 class를 구현하고 있다.
이때 interface에서 정의한 멤버변수나 메소드를 구현하지 않으면 error가 발생한다.
// bad interface Person { name: string; age: number; getIsYoungerThan(age: number): boolean; } class SomePerson implements Person { // error name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } }상속
또 interface는 extends 키워드를 이용해 다른 interface로 상속이 가능하다
interface Person { name: string; age: number; } interface Korean extends Person { isLiveInKorea: boolean; }위에서 Korean은
interface Korean { name: string; age: number; isLiveInKorea: boolean; }이 코드와 동일하다.
또한 여러 개의 interface를 동시에 상속받을 수 있다.
interface Person { name: string; age: number; } interface Programmer { favoriteProgrammingLanguage: string; } interface Korean extends Person, Programmer { isLiveInKorea: boolean; }위 코드에서 Korean은 Person과 Programmer를 상속받고 있으므로
Person과 Programmer에서 지정한 속성들을 모두 가지고 있다.
인터섹션 이용
interface Person { name: string; age: number; } interface Product { name: string; price: number; } type PP = Person & Product; const pp: PP = { name: 'a', age: 23, price: 1000 }위 코드 에서는 교차타입(
&)을 사용하고 있다. 교차타입은 교집합 관계를 나타낸다.interface에 교차타입을 이용하면 하나로 합칠 수 있는데,
따라서 PP는 Person과 Product의 속성값을 모두 가지고 있다.
PP가 name만 갖는 게 아니라 Person과 Product의 합처럼 기능한다는 점에 주의하자.
참고
- type vs interface: https://yceffort.kr/2021/03/typescript-interface-vs-type
- type vs interface2: https://medium.com/humanscape-tech/type-vs-interface-%EC%96%B8%EC%A0%9C-%EC%96%B4%EB%96%BB%EA%B2%8C-f36499b0de50
- 객체지향 프로그래밍: https://www.youtube.com/watch?v=vrhIxBWSJ04
- 인덱스 타입 타입호환성 패치사항: https://devblogs.microsoft.com/typescript/announcing-typescript-4-4-beta/#symbol-template-signatures
- 인덱스 타입 타입호환성 패치사항2: https://github.com/microsoft/TypeScript/pull/44512
8. 클래스
타입스크립트에서 class를 작성하는 법을 알아보자
class Person { name: string; age: number = 0; constructor(name: string, age: number) { this.name = name; this.age = age; } sayHello() { console.log('hello'); } }두 개의 멤버변수 정의.
객체가 생성될 떄 호출되며 멤버변수를 초기화 하는 생성자 정의.
sayHello라는 메소드를 하나 정의했다. (참고: 클래스나 객체 등에 프로퍼티로 정의되어 있는 함수를 메소드라고 한다.)
상속
class Person { name: string; age: number = 0; constructor(name: string, age: number) { this.name = name; this.age = age; } sayHello() { console.log('hello'); } } // 클래스 상속 class Programmer extends Person { fixBug() { console.log('버그 수정 완료'); } } const programmer = new Programmer('simi', 30); programmer.fixBug(); // '버그 수정 완료' programmer.sayHello(); // 'hello'위와 같이 상속도 가능하다.
Person 클래스를 상속받은 Programmer의 인스턴스 programmer는 fixBug뿐만 아니라 sayHello도 사용 가능하다.
super와 override
class Person { name: string; age: number = 0; constructor(name: string, age: number) { this.name = name; this.age = age; } sayHello() { console.log(`안녕하세요. 저는 ${this.name}이고 ${this.age}세입니다.`); } } class Programmer extends Person { constructor(name: string, age: number) { // super를 호출하지 않으면 error 발생 super(name, age); } sayHello() { // super를 호출하지 않아도 됨. 필요 시에만 작성. super.sayHello(); // super사용 시 부모 클래스의 sayHello 함수가 호출된다. console.log('저는 프로그래머입니다.'); } } class Docter extends Person { constructor(name: string, age: number) { super(name, age); } sayHello() { super.sayHello(); console.log('저는 의사입니다.'); } } const programmer = new Programmer('류시명', 30); programmer.sayHello(); // '안녕하세요. 저는 류시명이고 30세입니다.' \n '저는 프로그래머입니다.'자식 클래스에서 생성자를 작성할 때는 super를 호출해줘야 한다. 그렇지 않으면 error가 발생한다.
super를 호출하면 부모 클래스의 생성자가 호출된다.
또한 현재 부모 클래스(Person)에 sayHello가 있고 자식 클래스(Programmer)에도 sayHello가 있다.
이때 Programmer의 인스턴스가 sayHello를 호출하면 부모 클래스(Person)의 sayHello가 아니라
자식 클래스(Programmer)의 sayHello가 호출된다.
이를 method override라고 부른다.
자식 클래스의 메소드에는 super를 사용할 수 있으나 필수는 아니다.
접근 제한자
클래스의 멤버변수와 메소드는 접근제한자를 통해 접근 범위를 지정할 수 있다.
접근 제한자로는 publice, protect, private이며 접근 제한자를 지정하지 않으면 public이 기본값이다.
- publice: 외부에서 접근 가능, 자식 클래스에서 접근 가능
- private: 외부에서 접근 불가, 자식 클래스에서 접근 불가
- protected: 외부에서 접근 불가, 자식 클래스에서 접근 가능
class Person { private name: string; protected age: number = 0; constructor(name: string, age: number) { this.name = name; this.age = age; } sayHello() { console.log(`안녕하세요. 저는 ${this.name}이고 ${this.age}세입니다.`); } } class Programmer extends Person { constructor(name: string, age: number) { super(name, age); } sayHello() { super.sayHello(); this.name; // error this.age; // 사용가능 console.log('저는 프로그래머입니다.'); } } class Docter extends Person { constructor(name: string, age: number) { super(name, age); } sayHello() { super.sayHello(); console.log('저는 의사입니다.'); } } const programmer = new Programmer('류시명', 30); programmer.sayHello(); console.log(programmer.name); // error console.log(programmer.age); // errorprivate 제한자는
#을 이용해서도 표현할 수 있다.이는 비교적 최근에 추가된 자바스크립트 표준 문법으로
자바스크립트가 동적 타이핑 언어여서 private 키워드를 사용하지 못한 것으로 보인다.
정의와 사용 모두 #을 앞에 붙여서 사용하면 된다.
class Person { #name: string; constructor(name: string) { this.#name = name; } sayHello() { console.log(`안녕하세요. 저는 ${this.#name}입니다.`); } }위 코드는 아래와 정확히 같다.
class Person { private name: string; constructor(name: string) { this.name = name; } sayHello() { console.log(`안녕하세요. 저는 ${this.name}입니다.`); } }따라서 뭘 사용할지 하나 정해서 컨벤션에 따라 사용하면 된다.
protected의 활용
class Person { protected name: string; // 생성자 앞에 protected 선언 protected constructor(name: string) { this.name = name; } } class Programmer extends Person { sayHello() { console.log(`안녕하세요. 저는 ${this.name}입니다.`); } } const person = new Person('류시명'); // error console.log(person.name); // errorprotected를 생성자 앞에 선언하게 되면 해당 클래스는 외부에서 객체를 만들 수 없는 클래스가 된다.
이때 Person은 다른 클래스의 부모 역할만 할 수 있는 클래스가 된다.
readonly
class Person { readonly name: string; private readonly age: number; // 접근 제한자와 같이 사용 constructor(name: string, age: number) { // 생성자에서는 수정 가능 this.name = name; this.age = age; } } const person = new Person('류시명', 30); person.name = '시미'; // errorreadonly 키워드를 이용해서 수정이 불가능하게 만들 수도 있다.
단 생성자에서는 수정이 가능하다.
또 readonly 키워드는 다른 접근 제한자와 같이 사용이 가능하다.
class 초기화 숏컷
지금까지는 멤버변수를 정의할 때 위에 멤버변수를 정의하고, 생성자 안에서 초기화 해주는 과정을 했다.
하지만 이 과정은 매우 반복적이고, 번거롭기 때문에 TS에서 편의 기능을 제공한다.
constructor를 정의할 때 파라미터에 readonly나 접근 제한자를 사용하면 해당 파라미터는 자동으로 멤버변수가 된다.
class Person { constructor(readonly name: string, private readonly age: number) {} } // 위 코드는 아래 코드와 같다 class Person { readonly name: string; private readonly age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } }getter와 setter
class Person { private _name: string = ''; // getter get name(): string { console.log('getter called'); return this._name; } // setter set name(newName: string) { if (newName.length > 10) { throw new Error('최대 길이를 넘었습니다.'); } this._name = newName; } } const person = new Person(); person.name = '홍길동'; // setter 호출 console.log(person.name); // getter 호출 -> '홍길동' 출력 person.name = 'asdfasdfasdfasdfasdfasdf'; // error. '최대 길이를 넘었습니다.'getter와 setter는 위와 같이 설정할 수 있다.
getter와 setter를 사용할 때 멤버변수에 언더바(_)를 붙인 건 관례이다.
static
static 키워드를 이용해서 정적 멤버변수와 정적 메소드를 만들 수 있다.
정적인 값들은 각 인스턴스와늰 무관하게 고정된 값이다.
사용 시에는
클래스.스테틱값형식으로 사용할 수 있다.class Person { static adultAge = 20; constructor(public name: string, public age: number) {} sayHello() { // name과 age는 정적 멤버변수가 아니기 때문에 인스턴스(this)를 통해 접근하고 있다. console.log(`안녕, 난 ${this.name}입니다.`); console.log(Person.getIsAdult(this.age) ? '성인입니다.' : '미성년자입니다.'); } static getIsAdult(age: number): boolean { return Person.adultAge <= age; } } const person = new Person('류시명', 30); person.sayHello(); console.log(`성인 판단 기준 나이: ${Person.adultAge}`)추상 클래스, 추상 메소드
// 추상 클래스 abstract class Person { constructor(public name: string) {} sayHello() { console.log(`안녕하세요 난 ${this.name}입니다.`); } // 추상 메소드 abstract sayHello2(): void; } class Programmer extends Person { // 부모 클래스에서 정의된 추상메소드는 자식클래스에서 반드시 정의되어야 함. sayHello2() { console.log('난 프로그래머입니다.'); } } const person = new Person('류시명'); // error. 추상클래스는 인스턴스를 만들 수 없다. const programmer = new Programmer('류시명'); programmer.sayHello2(); // '난 프로그래머입니다.'abstract 키워드를 이용하여 추상 클래스, 추상 메소드를 만들 수 있다.
추상 클래스는 인스턴스를 생성할 수 없다.
추상 메소드는 부모클래스에서는 내용을 정의할 수 없고, 자식클래스에서는 반드시 정의되어야 한다.
참고
- 함수 vs 메소드: https://velog.io/@canonmj/%ED%95%A8%EC%88%98%EC%99%80-%EB%A9%94%EC%84%9C%EB%93%9C%EC%9D%98-%EC%B0%A8%EC%9D%B4%EB%8A%94-object-%EC%8B%AC%ED%99%94%EC%9D%B4%ED%95%B4
- 함수 vs 메소드2: https://ffoorreeuunn.tistory.com/149
- 함수 vs 메소드3: https://zeddios.tistory.com/233
- super: https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/super
- 오버라이드, 오버로드: https://webclub.tistory.com/404
- JS getter, setter 사용: https://mygumi.tistory.com/161
반응형'Programming > Typescript' 카테고리의 다른 글
04-생산성을높이는타입스크립트의기능 (0) 2021.11.22 03-타입스크립트고급기능 (0) 2021.11.22 02-타입호환성 (0) 2021.11.22 00. 타입스크립트 시작하기 (0) 2021.11.22