2020-11-11
TypeScript
목표
TypeScript로 타이핑을 잘하면, 런타임 전에 미리 오류를 알 수 있습니다.
코드의 구현자가 사용자에게 의도를 전달할 수 있다.
타입 시스템
컴파일러에게 사용하는 타입을 명시적으로 지정하는 시스템
컴파일러가 자동으로 타입을 추론하는 시스템
☝ 타입스크립트는 타입을 명시적으로 지정할 수 있고 명시적으로 지정하지 않으면컴파일러가 자동으로 타입을 추론한다.
명시적 타입 vs 추론
// 함수의 리턴 타입은 number로 추론된다.
// 하지만 매개변수의 타입을 명시적으로 지정하지 않아서, any로 추론된다.
function f(a) {
return a * 38;
}
// 사용자는 a가 any 이기 때문에 사용법에 맞게 함수를 실행했지만, 사용자는 예측하지 못한 결과를 얻게 된다.
console.log(f(10)); // 380
console.log(f('Mark')); // NaN
// 함수의 리턴 타입은 number로 추론된다.
// 하지만 매개변수의 타입을 명시적으로 지정하지 않아서, any로 추론된다.
function f(a) {
return a * 38;
}
// 사용자는 a가 any 이기 때문에 사용법에 맞게 함수를 실행했지만, 사용자는 예측하지 못한 결과를 얻게 된다.
console.log(f(10)); // 380
console.log(f('Mark')); // NaN
이것은 noImplicitAny
옵션을 통해 방어할 수 있다. 타입을 명시적으로 지정하지 않은 경우 any
로 추론되는 변수가 있으면 컴파일 에러가 발생한다.
// 매개변수의 타입을 명시적으로 지정했다.
// 명시적으로 지정하지 않은 리턴 타입은 number로 추론된다.
function f(a: number) {
if (a > 0) {
return a * 38;
}
}
// 사용자는 사용법에 맞게 숫자형을 사용해서 함수를 실행했지만,
// 실제로는 함수의 리턴이 undefined로, undefined + 5가 실행되어 NaN이 출력된다.
console.log(f(5)); // 190;
console.log(f(-5) + 5); // NaN
// 매개변수의 타입을 명시적으로 지정했다.
// 명시적으로 지정하지 않은 리턴 타입은 number로 추론된다.
function f(a: number) {
if (a > 0) {
return a * 38;
}
}
// 사용자는 사용법에 맞게 숫자형을 사용해서 함수를 실행했지만,
// 실제로는 함수의 리턴이 undefined로, undefined + 5가 실행되어 NaN이 출력된다.
console.log(f(5)); // 190;
console.log(f(-5) + 5); // NaN
strictNullChecks
옵션을 켜면 모든 타입에 자동으로 포함되어 있는 null
과 undefined
를 제거해준다.
// 이 함수의 리턴 타입은 number | undefined로 추론된다.
function f(a: number) {
if (a > 0) {
return a * 38;
}
}
// 해당 함수의 리턴 타입은 number | undefined 이기 때문에,
// 타입에 따르면 컴파일 에러가 발생한다.
// 컴파일 에러를 고치기 위해 사용자와 작성자가 의논해야 한다.
console.log(f(-5) + 5); // error TS2532: Object is possibly 'undefined'
// 이 함수의 리턴 타입은 number | undefined로 추론된다.
function f(a: number) {
if (a > 0) {
return a * 38;
}
}
// 해당 함수의 리턴 타입은 number | undefined 이기 때문에,
// 타입에 따르면 컴파일 에러가 발생한다.
// 컴파일 에러를 고치기 위해 사용자와 작성자가 의논해야 한다.
console.log(f(-5) + 5); // error TS2532: Object is possibly 'undefined'
하지만 가급적이면 명시적으로 리턴 타입을 지정하는 것이 좋다.
noImplicitReturns
옵션을 켜면 함수 내의 모든 코드의 줄기가 값을 리턴하지 않으면 컴파일 에러를 발생시킨다.
// error TS7030: Not all code paths return a value
function f(a: number) {
if (a > 0) {
return a * 38;
}
}
// error TS7030: Not all code paths return a value
function f(a: number) {
if (a > 0) {
return a * 38;
}
}
이로 인해 리턴 타입을 명시하고, 모든 줄기에 return
을 직접 하도록 구현자에게강제할 수 있다.
매개변수에 object가 들어오는 경우
function f(a) {
return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
console.log(f({ name: 'Mark', age: 38 })); // 이름은 Mark이고, 연령대는 30대 입니다.
console.log(f('Mark')); // 이름은 undefined이고, 연령대는 NaN대 입니다.
function f(a) {
return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
console.log(f({ name: 'Mark', age: 38 })); // 이름은 Mark이고, 연령대는 30대 입니다.
console.log(f('Mark')); // 이름은 undefined이고, 연령대는 NaN대 입니다.
JavaScript에서는 제약사항으로 알려주지 않기 때문에 런타임에서 오류를 파악할 수있다.
- Object literal type
function f(a: { name: string, age: number }): string {
return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
function f(a: { name: string, age: number }): string {
return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
- 나만의 타입을 만드는 방법
interface PersonInterface {
name: string;
age: number;
}
type PersonTypeAlias = {
name: string;
age: number;
}
function f1(a: PersonInterface): string {
return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
function f2(a: PersonTypeAlias): string {
return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
interface PersonInterface {
name: string;
age: number;
}
type PersonTypeAlias = {
name: string;
age: number;
}
function f1(a: PersonInterface): string {
return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
function f2(a: PersonTypeAlias): string {
return `이름은 ${a.name}이고 연령대는 ${Math.floor(a.age / 10) * 10)}대 입니다.`
}
interface vs type alias
structural vs nominal type system
타입스크립트는 structural 타입 시스템을 사용한다.
structural type system : 구조가 같으면, 같은 타입이다.
nominal type system : 구조가 같아도 이름이 다르면, 다른 타입이다.
interface IPerson {
name: string;
age: number;
speak(): string;
}
type PersonType = {
name: string;
age: number;
speak(): string;
};
interface IPerson {
name: string;
age: number;
speak(): string;
}
type PersonType = {
name: string;
age: number;
speak(): string;
};
typescript를 nominal 처럼 쓰는 꼼수 ? - 아직 잘 이해를 못함
typescripttype PersonID = string & {readonly brand: unique symbol}; function PersonID(id: string): PersonID { return id as PersonID; } function getPersonById(id: PersonID) {} getPersonById(PersonID('id-aaaaaa')); getPersonById('id-aaaaaa');
type PersonID = string & {readonly brand: unique symbol}; function PersonID(id: string): PersonID { return id as PersonID; } function getPersonById(id: PersonID) {} getPersonById(PersonID('id-aaaaaa')); getPersonById('id-aaaaaa');
function
typescripttype EatType = (food: string) => void; interface IEat { (food: string): void; }
type EatType = (food: string) => void; interface IEat { (food: string): void; }
array
typescripttype PersonList = string[]; interface IPersonList { [index: number]: string; }
type PersonList = string[]; interface IPersonList { [index: number]: string; }
intersection
typescriptinterface ErrorHandling { success: boolean; error?: {message: string}; } interface ArtistsData { artists: {name: string}[]; } type ArtistsResponseType = ArtistsData & ErrorHandling; interface IArtistsResponse extends ArtistsData, ErrorHandling {}
interface ErrorHandling { success: boolean; error?: {message: string}; } interface ArtistsData { artists: {name: string}[]; } type ArtistsResponseType = ArtistsData & ErrorHandling; interface IArtistsResponse extends ArtistsData, ErrorHandling {}
union
typescriptinterface Bird { fly(): void; layEggs(): void; } interface Fish { swim(): void; layEggs(): void; } type BirdType = { fly(): void; layEggs(): void; }; type FishType = { swim(): void; layEggs(): void; }; // union은 type alias를 사용해야 한다. type PetType = Bird | Fish; // 에러 발생 interface IPet extends PetType {} class Pet implements PetType {} // 가능 interface IBird extends BirdType {} class IFish implements FishType {}
interface Bird { fly(): void; layEggs(): void; } interface Fish { swim(): void; layEggs(): void; } type BirdType = { fly(): void; layEggs(): void; }; type FishType = { swim(): void; layEggs(): void; }; // union은 type alias를 사용해야 한다. type PetType = Bird | Fish; // 에러 발생 interface IPet extends PetType {} class Pet implements PetType {} // 가능 interface IBird extends BirdType {} class IFish implements FishType {}
Declaration Merging - interface
중복되는 이름으로 선언되었을 때 머지되는 기능은 interface에서만 제공된다.
type은 중복시에 에러가 발생한다.
이는 외부 유틸에서 사용자가 원하는 타입을 추가해서 사용하는 것이 가능하다.
interface MergingInterface {
a: string;
}
interface MergingInterface {
b: string;
}
let mi: MergingInterface;
// mi: { a: string, b: string }
interface MergingInterface {
a: string;
}
interface MergingInterface {
b: string;
}
let mi: MergingInterface;
// mi: { a: string, b: string }
언제 type을 사용하고 언제 interface를 사용하는가
의미적으로, 역할적으로 type alias를 타입에 별칭을 붙일 때 사용한다.
즉, 이미 있는 타입에 별칭을 붙여 다른 이름으로 부르고 싶을 때, 이미 있는 타입을 union으로 조합할 때 사용한다.
그 외에 새로운 타입을 생성할 때 interface를 사용한다.
- 실제 동작 상의 차이는 Merging에서의 차이가 가장 중요하다.
서브 타입과 슈퍼 타입
집합의 관계에서 포함되는 쪽이 서브 타입, 포함하는 쪽이 슈퍼 타입이다.
let sub1: 1 = 1;
let sup1: number = sub1;
sub1 = sup1; // error! Type 'number' is not assignable to type '1'.
let sub2: number[] = [1];
let sup2: object = sub2;
sub2 = sup2; // error! Type '{}' is missing the following the properties from type 'number[]': length, pop, push, concat, and 16 more.
let sub3: [number, number] = [1, 2];
let sup3: number[] = sub3;
sub3 = sup3; // error! Type 'number[]' is not assignable to type '[number, number]'. Target requires 2 element(s) but source may have fewer.
let sub4: number = 1;
let sup4: any = sub4;
sub4 = sup4;
let sub5: never = 0 as never;
let sup5: number = sub5;
sub5 = sup5; // Type 'number' is not assignable to type 'never'.
class Animal {}
class Dog extends Animal {
eat() {}
}
let sub6: Dog = new Dog();
let sup6: Animal = sub6;
sub6 = sup6; // Property 'eat' is missing in type 'Animal' but required in type 'Dog'.
let sub1: 1 = 1;
let sup1: number = sub1;
sub1 = sup1; // error! Type 'number' is not assignable to type '1'.
let sub2: number[] = [1];
let sup2: object = sub2;
sub2 = sup2; // error! Type '{}' is missing the following the properties from type 'number[]': length, pop, push, concat, and 16 more.
let sub3: [number, number] = [1, 2];
let sup3: number[] = sub3;
sub3 = sup3; // error! Type 'number[]' is not assignable to type '[number, number]'. Target requires 2 element(s) but source may have fewer.
let sub4: number = 1;
let sup4: any = sub4;
sub4 = sup4;
let sub5: never = 0 as never;
let sup5: number = sub5;
sub5 = sup5; // Type 'number' is not assignable to type 'never'.
class Animal {}
class Dog extends Animal {
eat() {}
}
let sub6: Dog = new Dog();
let sup6: Animal = sub6;
sub6 = sup6; // Property 'eat' is missing in type 'Animal' but required in type 'Dog'.
☝
any
와never
는 추후에 다시 학습
같거나 서브 타입인 경우 할당이 가능하다. ⇒ 공변
let sub7: string = '';
let sup7: string | number = sub7;
// object - 각각의 프로퍼티가 대응하는 프로퍼티와 같거나 서브타입이어야 한다.
let sub8: {a: string; b: number} = {a: '', b: 1};
let sup8: {a: string | number; b: number} = sub8;
// array - object와 마찬가지
let sub9: Array<{a: string; b: number}> = [{a: '', b: 1}];
let sup9: Array<{a: string | number; b: number}> = sub9;
let sub7: string = '';
let sup7: string | number = sub7;
// object - 각각의 프로퍼티가 대응하는 프로퍼티와 같거나 서브타입이어야 한다.
let sub8: {a: string; b: number} = {a: '', b: 1};
let sup8: {a: string | number; b: number} = sub8;
// array - object와 마찬가지
let sub9: Array<{a: string; b: number}> = [{a: '', b: 1}];
let sup9: Array<{a: string | number; b: number}> = sub9;
함수의 매개변수 타입만 같거나 슈퍼타입인 경우, 할당이 가능하다 ⇒ 반병
class Person {}
class Developer extends Person {
coding() {}
}
class StartupDeveloper extends Developer {
burning() {}
}
function tellme(f: (d: Developer) => Developer) {}
// Developer => Developer 에다가 Developer => Developer 를 할당하는 경우
tellme(function dToD(d: Developer): Developer {
return new Developer();
});
// Developer => Developer 에다가 Person => Developer 를 할당하는 경우
// 반병
tellme(function pToD(d: Person): Developer {
return new Developer();
});
// Developer => Developer 에다가 StartupDeveloper => Developer 를 할당하는 경우
// strictFunctionType으로 에러 표시 가능
tellme(function sToD(d: StartupDeveloper): Developer {
return new Developer();
});
class Person {}
class Developer extends Person {
coding() {}
}
class StartupDeveloper extends Developer {
burning() {}
}
function tellme(f: (d: Developer) => Developer) {}
// Developer => Developer 에다가 Developer => Developer 를 할당하는 경우
tellme(function dToD(d: Developer): Developer {
return new Developer();
});
// Developer => Developer 에다가 Person => Developer 를 할당하는 경우
// 반병
tellme(function pToD(d: Person): Developer {
return new Developer();
});
// Developer => Developer 에다가 StartupDeveloper => Developer 를 할당하는 경우
// strictFunctionType으로 에러 표시 가능
tellme(function sToD(d: StartupDeveloper): Developer {
return new Developer();
});
any
입력은 마음대로
함수 구현이 자유롭게 ⇒ 자유가 항상 좋은 건 아니다.
unknown
입력은 마음대로
함수 구현은 문제 없도록
타입 추론 이해하기
let과 const의 타입 추론 (+ as const)
as const
let a = 'Mark'; // string
const b = 'Mark'; // 'Mark' => literal type
let c = 38; // number
const d = 38; // 38 => literal type
let e = false; // boolean
const f = false; // false => literal type
let g = ['Mark', 'Haeun']; // string[]
const h = ['Mark', 'Haeun']; // string[]
const i = ['Mark', 'Haeun', 'Bokdang'] as const; // readonly ['Mark', 'Haeun', 'Bokdang']
let a = 'Mark'; // string
const b = 'Mark'; // 'Mark' => literal type
let c = 38; // number
const d = 38; // 38 => literal type
let e = false; // boolean
const f = false; // false => literal type
let g = ['Mark', 'Haeun']; // string[]
const h = ['Mark', 'Haeun']; // string[]
const i = ['Mark', 'Haeun', 'Bokdang'] as const; // readonly ['Mark', 'Haeun', 'Bokdang']
- best common type (가장 공통적인 타입을 추론해낸다)
let j = [0, 1, null]; // (number | null)[]
const k = [0, 1, null]; // (number | null)[]
class Animal {}
class Rhino extends Animal {}
class Elephant extends Animal {}
class Snake extends Animal {}
let l = [new Rhino(), new Elephant(), new Snake()]; // (Rhino | Elephant | Snake)[]
const m = [new Rhino(), new Elephant(), new Snake()]; // (Rhino | Elephant | Snake)[]
const n = [new Animal(), new Rhino(), new Elephant(), new Snake()]; // Animal[]
const o: Animal[] = [new Rhino(), new Elephant(), new Snake()]; // Animal[]
let j = [0, 1, null]; // (number | null)[]
const k = [0, 1, null]; // (number | null)[]
class Animal {}
class Rhino extends Animal {}
class Elephant extends Animal {}
class Snake extends Animal {}
let l = [new Rhino(), new Elephant(), new Snake()]; // (Rhino | Elephant | Snake)[]
const m = [new Rhino(), new Elephant(), new Snake()]; // (Rhino | Elephant | Snake)[]
const n = [new Animal(), new Rhino(), new Elephant(), new Snake()]; // Animal[]
const o: Animal[] = [new Rhino(), new Elephant(), new Snake()]; // Animal[]
- Contextual Typing - 위치에 따라 추론이 다름
// Parameter 'e' implicitly has an 'any' type.
const click = (e) => {
e; // any
};
document.addEventListener('click', click);
document.addEventListener('click', (e) => {
e; // MouseEvent
});
// Parameter 'e' implicitly has an 'any' type.
const click = (e) => {
e; // any
};
document.addEventListener('click', click);
document.addEventListener('click', (e) => {
e; // MouseEvent
});
Type Guard로 안전함을 파악하기
typeof Type Guard - 보통 Primitive 타입일 경우
- typeof로 primitive 타입을 걸러낼 수 있다.
function getNumber(value: number | string): number {
value; // number | string
if (typeof value == 'number') {
return value;
}
value; // string
return -1;
}
function getNumber(value: number | string): number {
value; // number | string
if (typeof value == 'number') {
return value;
}
value; // string
return -1;
}
instanceof Type Guard - Error 객체 구분에 많이 쓰인다.
class NegativeNumberError extends Error {}
function getNumber(value: number): number | NegativeNumberError {
if (value < 0) return new NegativeNumberError();
return value;
}
function main() {
const num = getNumber(-10);
if (num instanceof NegativeNumberError) {
return;
}
num; // number
}
class NegativeNumberError extends Error {}
function getNumber(value: number): number | NegativeNumberError {
if (value < 0) return new NegativeNumberError();
return value;
}
function main() {
const num = getNumber(-10);
if (num instanceof NegativeNumberError) {
return;
}
num; // number
}
in operator Type Guard - object의 프로퍼티 유무로 처리하는 경우
interface Admin {
id: string;
role: string;
}
interface User {
id: string;
emain: string;
}
function redirect(user: Admin | User) {
if ('role' in user) {
routeToAdminPage(user.role);
} else {
routeToHomePage(user.email);
}
}
interface Admin {
id: string;
role: string;
}
interface User {
id: string;
emain: string;
}
function redirect(user: Admin | User) {
if ('role' in user) {
routeToAdminPage(user.role);
} else {
routeToHomePage(user.email);
}
}
literal Type Guard - object의 프로퍼티가 같고, 타입이 다른 경우
interface IMachine {
type: string;
}
class Car implements IMachine {
type: 'CAR';
wheel: number;
}
class Boat implements IMachine {
type: 'BOAT';
motor: number;
}
function getWheelOrMotor(machine: Car | Boat): number {
if (machine.type === 'CAR') {
return machine.wheel;
} else {
return machine.motor;
}
}
interface IMachine {
type: string;
}
class Car implements IMachine {
type: 'CAR';
wheel: number;
}
class Boat implements IMachine {
type: 'BOAT';
motor: number;
}
function getWheelOrMotor(machine: Car | Boat): number {
if (machine.type === 'CAR') {
return machine.wheel;
} else {
return machine.motor;
}
}
custom Type Guard
function getWheelOrMoter(machine: any): number {
if (isCar(machine)) {
return machine.wheel;
} else if (isBoat(machine)) {
return machine.motor;
} else {
return -1;
}
}
function isCar(arg: any): arg is Car {
return arg.type === 'CAR';
}
function isBoat(arg: any): arg is Boat {
return arg.type === 'BOAT';
}
function getWheelOrMoter(machine: any): number {
if (isCar(machine)) {
return machine.wheel;
} else if (isBoat(machine)) {
return machine.motor;
} else {
return -1;
}
}
function isCar(arg: any): arg is Car {
return arg.type === 'CAR';
}
function isBoat(arg: any): arg is Boat {
return arg.type === 'BOAT';
}
Conditional Type
`Item T`
typescriptinterface StringContainer { value: string; format(): string; split(): string[]; } interface NumberContainer { value: number; nearestPrime: number; round(): number; } type Item1<T> = { id: T; container: any; }; const item1: Item1<string> = { id: 'aaaaaa', container: null, // container type에 any를 사용했기 때문에 null을 넣는 것도 가능하다. };
interface StringContainer { value: string; format(): string; split(): string[]; } interface NumberContainer { value: number; nearestPrime: number; round(): number; } type Item1<T> = { id: T; container: any; }; const item1: Item1<string> = { id: 'aaaaaa', container: null, // container type에 any를 사용했기 때문에 null을 넣는 것도 가능하다. };
`Item T` - T가 string이면 StringContainer, 아니면 NumberContainer
typescripttype Item2<T> = { id: T; container: T extends string ? StringContainer : NumberContainer; }; const item2: Item2<string> = { id: 'aaaaaa', container: null, // Type 'null' is not assignable to type 'StringContainer'. };
type Item2<T> = { id: T; container: T extends string ? StringContainer : NumberContainer; }; const item2: Item2<string> = { id: 'aaaaaa', container: null, // Type 'null' is not assignable to type 'StringContainer'. };
`Item T` - T가 string이면 StringContainer, number면 NumberContainer, 아니면 사용 불가
typescripttype Item3<T> = { id: T extends string | number ? T : never; container: T extends string ? StringContainer : T extends number ? NumberContainer : never; }; const item3: Item3<boolean> = { id: true, // Type 'boolean' is not assignable to type 'never'. container: null, // Type 'null' is not assignable to type 'never'. };
type Item3<T> = { id: T extends string | number ? T : never; container: T extends string ? StringContainer : T extends number ? NumberContainer : never; }; const item3: Item3<boolean> = { id: true, // Type 'boolean' is not assignable to type 'never'. container: null, // Type 'null' is not assignable to type 'never'. };
`ArrayFilter T`
typescripttype ArrayFilter<T> = T extends any[] ? T : never; type StringsOrNumbers = ArrayFilter<string | number | string[] | number[]>;
type ArrayFilter<T> = T extends any[] ? T : never; type StringsOrNumbers = ArrayFilter<string | number | string[] | number[]>;
1:11:12 까지 시청 ...