Type System 정리

일상 코딩·2022년 4월 7일
0

TypeScript

목록 보기
7/12

01.작성자와 사용자의 관점으로 코드 바라보기

1-1.타입 시스템 이란?

  • 컴파일러에게 사용하는 타입을 명시적으로 지정하는 시스템
  • 컴파일러가 자동으로 타입을 추론하는 시스템

1-2.타입스크립트의 타입 시스템

  • 타입을 명시적으로 지정할 수 있다.
  • 타입을 명시적으로 지정하지 않으면, 타입 스크립트 컴파일러가 자동으로 타입을 추론

1-3.자신의 코드에서 해당 함수를 사용하는 사용자 vs 해당 함수를 구현하는 구현자

  • 타입이란 해당 변수가 할 수 있는 일을 결정합니다.
// f1 이라는 함수의 body 에서는 a를 사용할 것 입니다.
// a가 할 수 있는 일은 a의 타입이 결정합니다.
function f1(a) {
  return a;
}
  • 함수 사용법에 대한 오해를 야기하는 자바스크립트
// JavaScript
// (f2 실행의 결과가 NaN 을 의도한 것이 아니라면)
// 이 함수의 작성자는 매개변수 a 가 number타입이라는 가정으로 함수를 작성했습니다.
function f2(a) {
	return a * 38;
}
// 사용자는 사용법을 숙지하지 않은 채, 문자열을 사용하여 함수를 실행했습니다.
console.log(f2(10); // 380
console.log(f2('Mark')); // NaN
  • 타입스크립트의 추론에 의지하는 경우
// 타입스크립트 코드지만,
// a의 타입을 명시적으로 지정하지 않은 경우이기 때문에 a는 any로 추론됩니다.
// 함수의 리턴 타입은 number로 추론됩니다. (NaN도 number의 하나입니다.)
function f3(a) {
	return a * 38;
}
// 사용자는 a가 any이기 때문에, 문자열을 사용하여 함수를 실행했습니다.
console.log(f3(10); // 380
console.log(f3('Mark') + 5); // NaN

1-4.noImplicitAny 옵션

  • noImplicitAny 옵션을 켜면 타입을 명시적으로 지정하지 않은 경우, 타입스크립트가 추론 중 any라고 판단하게 되면, 컴파일 에러를 발생시켜 명시적으로 지정하도록 유도한다.
  • nolmplicitAny에 의한 방어
// error Ts7006: Parameter 'a' implicitly has an 'any' type.
function f3(a) {
	return a * 38;
}
// 사용자의 코드를 실행할 수 없습니다. 컴파일이 정상적으로 마무리 될 수 있도록 수정해야합니다.
console.log(f3(10));
console.log(f3('Mark') + 5);
  • 함수의 매개변수에 타입을 명시적으로 지정하지 않았기 때문에 컴파일 에러 메시지가 출력된다.
  • number 타입을 추론된 리턴 타입
// 매개변수의 타입은 명시적으로 지정했습니다.
// 명시적으로 지정하지 않은 함수의 리턴 타입은 number로 추론됩니다.
function f4(a: number) {
	if (a > 0) {
	return a * 38;
	}
}
// 사용자는 사용법에 맞게 숫자형을 사용하여 함수를 실행했습니다.
// 해당 함수의 리턴 타입은 number이기 때문에, 타입에 따르면 이어진 연산을 바로 할 수 있습니다.
// 하지만 실제 undefined + 5 가 실행되어 NaN 이 출력됩니다.
console.log(f4(5)); // 190
console.log(ft(-5) + 5); // NaN
  • 매개변수 a값이 0보다 크지 않으면 리턴하지 않기 때문에 undefined값이 리턴됩니다.
  • 함수의 매개변수에만 타입을 지정하고 함수의 리턴 타입은 따로 지정하지 않았으나 명시적으로 지정하지 않은 함수의 리턴 타입은 number로 추론됩니다.
  • TypeScript에서는 number안에 undefined가 포함되어져 있습니다.

1-5.strictNullChecks 옵션

  • strictNullChecks 옵션을 켜면 모든 타입에 자동으로 포함되어 있는 nullundefined를 제거해줍니다.
  • number | undefined 타입으로 추론된 리턴 타입
// 매개변수의 타입은 명시적으로 지정했습니다.
// 명시적으로 지정하지 않은 함수의 리턴 타입은 number | undefined 로 추론됩니다.
function f4(a: number) {
	if (a > 0) {
	return a * 38;
	}
}
// 사용자는 사용법에 맞게 숫자형을 사용하여 함수를 실행했습니다.
// 해당 함수의 리턴 타입은 number | undefined 이기 때문에,
// 타입에 따르면 이어진 연산을 바로 할 수 없습니다.
// 컴파일 에러를 고쳐야하기 하기 때문에 사용자와 작성자가 의논을 해야합니다.
console.log(f4(5));
console.log(f4(-5) + 5); // error TS2532: Object is possibly 'undefined'.

1-6.명시적으로 리턴 타입을 지정해야 할까?

  • 리턴부의 타입과 함수 내의 타입을 비교해 일치하는지 TS가 봐줍니다.
  • 하지만 명시적으로 리턴 타입을 지정하며 코드를 작성하는 것을 권장합니다.
// 매개변수의 타입과 함수의 리턴 타입을 명시적으로지정했습니다.
// 실제 함수 구현부의 리턴 타입과 명시적으로 지정한 타입이 일치하지 않아 컴파일 에러가 발생합니다.
// error TS2366: Function lacks ending return statement and return type does not inc
function f5(a: number): number {
	if (a > 0) {
	return a * 38;
	} // if 가 아닌 부분은 작성이 덜 된 것 같다고 에러 발생
}

1-7.noImplicitReturns 옵션

  • noImplicitReturns 옵션을 켜면 함수 내에서 모든 코드가 리턴 값을 리턴하지 않으면 컴파일 에러를 발생시킨다.
  • 모든 코드에서 리턴을 직접해야한다.
// if 가 아닌 경우 return을 직접 하지 않고 코드가 종료된다.
// error TS7030: Not all code paths return a value.
function f5(a: number) {
	if (a > 0) {
	return a * 38;
}
  • 매개변수에 object가 들어오는 경우
// JavaScript
function f6(a) {
	return `이름은 ${a.name} 이고, 연령대는 ${
	Math.floor(a.age / 10) * 10
	}대 입니다.`;
}
console.log(f6({ name: 'Mark', age: 38 })); // 이름은 Mark이고, 연령대는 30대 입니다.
console.log(f6('Mark')); // 이름은 undefined이고, 연령대는 NaN대 입니다.
  • JS코드에서 매개변수 aobject타입이 아니더라도 에러 메시지가 출력되지않고 잘못된 결과가 출력되는 문제 발생
  • object literal type
function f7(a: {name: string; age: number): string {
	return `이름은 ${a.name} 이고, 연령대는 ${
	Math.floor(a.age / 10) * 10
	}대 입니다.`;
}
console.log(f7({ name: 'Mark', age: 38 })); // 이름은 Mark이고, 연령대는 30대 입니다.
console.log(f6('Mark')); // error TS2345: Argument of type 'string' is not
// assignable to parameter of type '{ name: string; age: number; }'.
  • TS코드에서 매개변수 aobject를 정확히 명시적으로 지정하여 잘못된 값이 매개변수에 입력되면 에러 메시지 출력

1-8.나만의 타입을 만드는 방법 (TS 주요 기능)

  • 타입을 계속해서 작성해주는 불편함을 해소시켜 줍니다.
// 예시
interface PersonInterface {
	name: string;
	age: number;
}
type PersonTypeAlias = {
	name: string;
	age: number;
};
function f8(a: PersonInterface): string {
	return `이름은 ${a.name} 이고, 연령대는 ${
	Math.floor(a.age / 10) * 10
	}대 입니다.`;
}

console.log(f8({ name: 'Mark', age: 38 })); // 이름은 Mark이고, 연령대는 30대 입니다.
consoel.log(f8('Mark')); // error TS2345: Argument of type 'string' is not
// assignable to parameter of type 'PersonInterface'.

02.Structure Type VS Nominal Type

2-1.structural type system

  • 구조가 같으면, 같은 타입이다. (TS)
interface IPerson {
  name: string;
  age: number;
  speak(): string;
}
type PersonType = {
  name: string;
  age: number;
  speak(): string;
};

let personInterface: IPerson = {} as any;
let personType: PersonType = {} as any;

persontInterface = personType;
personType = personInterface;

2-2.nominal type system

  • 구조가 같아도 이름이 다르면, 다른 타입이다. (C, JAVA와 유사)
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'); // error TS2345: Argument of type 'string' is not
// assignable to parameter of type 'PersonID'. Type 'string' is not assignable to type
// '{ readonly brand: unique symbol; }'.

2-3.duck typing (duck testing에서 유래)

  • 타입을 미리 정하는게 아니라 실행이 되었을 때 해당 Method들을 확인하여 타입을 정한다.
  • 사람이 오리처럼 행동하면 오리로 봐도 무방하다라는게 덕 타이핑(Duck Typing)이다.
  • TSduck typing이 아닙니다. (Python과 유사)
class Duck:
	def sound(self):
		print u"꽥꽥"

class Dog:
	def sound(self):
		print u"멍멍"

def get_sound(animal):
	animal.sound()

def main():
	bird = Duck()
	dog = Dog()
	get_sound(bird)
	get_sound(dog)

03.타입 호환성

3-1.서브 타입1

// sub1 타입은 sup1 타입의 서브 타입이다.
let sub1: 1 = 1;
let sup1: number = sub1;
sub1 = sup1; // error! Type 'number' is not assignable to type'1'.

// sub2 타입은 sup2 타입의 서브타입이다.
let sub2: number[] = [1];
let sup2: object = sub2;
sub2 = sup2 // error! Type '{}' is missing the following properites form type
// 'number[]': length, pop, push, concat, and 16 more

// sub 3 타입은 sup3 타입의 서브 타입이다.
let sub3: [number, number] = [1, 2];
let sup3: numbr[] = sub3;
sub3 = sup3; // error! type 'number[]' is not assignable to type '[number, number]'.
// Target requires 2 element(s) but source may have fewer.

3-2.서브 타입2

// sub4 타입은 sup4 타입의 서브 타입이다.
let sub4: number = 1;
let sup4: any = sub4;
sub4 = sup4;

// sub5 타입은 sup5 타입의 서브 타입이다.
let sub5: never = 0 as never;
let sup5: number = sub5;
sub 5 = sup5 // error! Type 'number' is not assignable to type 'never'.

class Animal {}
class Dog extends Animal {
	eat() {}
}
// sub6 타입은 sup6 타입의 서브 타입이다.
let sub6: Dog = new Dog();
let sup6: Animal = sub6;
sub6 = sup6; // error! Property 'eat' is missing in type 'SubAnimal' but
// required in type 'SubDog'.

3-3.공변 - 같거나 서브 타입인 경우, 할당이 가능하다.

// primitive type
let sub7: string = '';
let sup: 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 }> = sub8;
  • 일반적으로 sub는 좁은 범위의 타입이고 sup는 넓은 범위의 타입이다.
  • subsup범위 안에 들어갈 수 있지만 supsub 범위안에 들어가면 에러가 발생한다.

3-4.반병 - 함수의 매개변수 타입만 같거나 슈퍼타입인 경우, 할당이 가능하다.

class Person {}
class Developer extends Person {
	coding() {}
}
class StartupDeveloper extends Developer {
	burning() {}
}

// tellme 함수 생성
function tellme(f: (d: Developer) => Developer) {} 

// tellme(Developer => Developer) 에다가 Developer => Developer 를 할당하는 경우
tellme(function dToD(d: Developer): Developer {
	return new Developer();
});

// tellme(Developer => Developer) 에다가 Person => Developer 를 할당하는 경우 (슈퍼 타입)
tellme(function pToD(d: Person): Developer {
	return new Developer();
});

// tellme(Developer => Developer) 에다가 StartupDeveloper => Developer 를 할당하는 경우 (서브 타입 => 옵션 활성화시 에러 발생)
tellme(function sToD(d: StartupDeveloper): Developer {
	return new Developer();
});
// 사용자에게 선택옵션을 줌. (옵션을 켜지 않으면 융통성을 발휘해서 에러가 발생하지 않음)
// strictFunctionTypes 옵션을 켜면 에러를 통해 경고한다.
  • strictFunctionTypes 옵션을 켜면 함수를 할당할 시에 함수의 매개변수 타입이 같거나 슈퍼타입인 경우가 아닌 경우, 에러를 통해 경고한다.
  • 기본적으로 TS는 타입 호환성 규칙으로 공변성을 갖고 있지만 함수의 매개변수는 반공변성을 갖고 있습니다.

04.타입 별칭(Type Alias)

  • type alias는 특정 타입이나 인터페이스를 참조할 수 있는 타입 변수를 의미합니다.
  • interface처럼 변수명 앞에 type이라는 표현을 사용합니다.
  • 변수명은 대문자로 시작합니다.
  • Interface와 기능적으로 유사해 보이지만 다릅니다.
  • 만들어지 타입의 refer(별명)로 사용하는 것이지 타입을 만드는 것은 아닙니다.
  • Primitive, Union Type, Tuple, Function

4-1.Aliasing Primitive

  • type alias에는 string이나 number 등의 기본 타입을 부여할 수 있다.
  • 기본적인 방식대로 변수에 직접 타입을 선언하면 다음과 같다.
const shoeSize: number = 270;
  • type alias를 사용하면 이렇게 표현할 수 있다.
type Size = number; // number 타입으로 Size 변수 생성!
const shoeSize: Size = 270; 

4-2.Aliasing Union Type

  • 기본적인 방식대로 변수에 직접 타입을 선언하면 다음과 같다.
let person: string | number = 0;
person = 'Mark';
  • type alias를 사용하면 이렇게 표현할 수 있다.
type StringOrNumber = string | number;
let another: StringOrNumber = 0;
another = 'Anna';
// 유니온 타입은 A도 가능하고 B도 가능한 타입

4-3.Aliasing Tuple

  • 기본적인 방식대로 변수에 직접 타입을 선언하면 다음과 같다.
let person: [string, number] = ['Mark', 35];
  • type alias를 사용하면 이렇게 표현할 수 있다.
type PersonTuple = [string, number];
let another: PersonTuple = ['Anna', 24];
let another2: PersonTuple = ['Top', 22];
// 튜플 타입에 별칭을 줘서 여러군데서 사용할 수 있다.

4-4.Aliasing Function

type EatType = (food: string) => void;

4-5.interface와의 비교

  • 4-5-1.선언 방식
interface Book {
  title: string;
  author: string;
  totalPage: number;
  isbn: number;
}

type Book = {
  title: string;
  author: string;
  totalPage: number;
  isbn: number;
}
  • type alias를 사용할 때는 등호(=)를 사용!
  • 4-5-2.개념적 차이
  • interfacestring, number처럼 아예 그 자체를 새로운 타입으로서 만드는 방식이다.
  • 그러나 type은 선언한 타입들을 변수처럼 단순히 참조하기 위해 이름을 부여한 것이다.
  • 4-5-3.확장성
  • type alias는 선언한 이후에 확장이 불가능하다.
  • 이에 반해 interfaceextends 표현식을 사용해 확장할 수 있다.
  • 이러한 확장성 측면에서, 타입을 선언할 때 type보다는 interface를 사용하는 것이 권장된다.
  • 4-5-4.interfaceAlias를 혼용해서 쓸 때 구분하는 법
  • 타입으로서의 목적이나 존재가치가 명확하면 interface 사용
  • 다른대상을 가리킨다거나 별명으로서만 존재한다면 Alias 사용
profile
일취월장(日就月將) - 「날마다 달마다 성장하고 발전한다.」

0개의 댓글