Typescript 입문

이민석·2023년 1월 22일
1

typescript

목록 보기
1/1

타입스크립트는 자바스크립트에 타입 문법을 추가한 언어이다.
자바스크립트로 프로젝트를 진행할때, 런타임에서 오류가 나는 경우가 많은데, 타입스크립트는 런타임에서 발생할 수 있는 오류를 컴파일에서 잡아줌으로써, 런타임에서 에러가 발생할 확률을 줄여주게 된다.
타입스크립트를 사용하면 런타임에서 모든 오류를 해결해주는 것이 아니라, 런타임에서 타입관련하여 발생할 수 있는 오류를 컴파일에서 발견할 수 있는 것이다!

본 포스팅은 타입스크립트 핸드북을 읽으면서, 들었던 의문과 답변에 대해 정리하였다.

tsc

typescript의 컴파일러는 tsc이다. tsc를 실행하면, .ts파일을 .js로 변환시켜준다. tsc는 대부분의 경우 사용자가 typescript보다 잘 알고 있을 것이라는 전제하에, ts파일에서 타입관련 오류가 있어도, 그대로 js 파일로 바꿔주게 된다.
strictNullChecks : null과 undefined 값을 참조하는 것을 방지하는 옵션
noImplicitAny : 타입을 any로 추론하게되면 오류를 발생시키는 옵션

interface vs type

interface와 type은 단독적으로 사용되었을때 매우 유사하게 동작한다.
다음과 같이 interface는 한번 선언된 이후에, 또다시 선언을 하게되면 새로운 field를 추가하는 것으로 이해한다.

interface Window {
  title: string
}

interface Window {
  ts: TypeScriptAPI
}

그러나 type의 경우에는 한번 선언된 이후에 변경될수 없기 때문에, 다음과 같은 상황을 오류로 인식한다.

type Window = {
  title: string
}

type Window = {
  ts: TypeScriptAPI
}

 // Error: Duplicate identifier 'Window'.

Call signatures and Construct signatures

type DescribableFunction = {
  description: string;
  (someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + " returned " + fn(6));
}
type SomeConstructor = {
  new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
  return new ctor("hello");
}

Index signatures

index signatures을 선언한 경우, 독립적으로 다른 property를 추가할때, index signature와 같은 key type을 사용하는 경우, value의 type도 index signature의 value type과 동일해야한다.

interface NumberDictionary {
  [index: string]: number;
 
  length: number; // ok
  name: string;
Property 'name' of type 'string' is not assignable to 'string' index type 'number'.
}

extend

extend는 확장하다라는 의미이다.

interface A {
  name : string,
}
interface B extends A{
  age : number
}

위와 같이 interface A와 B가 있을때, 필자는 B extends A 를 B가 A를 확장했다는 의미로 해석하고, B가 A를 포함하는 느낌으로 이해했다.
그러나 이는 완전히 반대였고, B extends A는 A가 B를 포함하는 것이고, B는 A의 부분집합이라고 볼 수 있는 것이다.

즉, extend를 사용하면 오히려 interface가 더 구체적으로? 제약조건이 추가로 생겨서 narrowing되는 느낌인 것이다.

이러한 의문은 제네릭 <T extends U> 에도 적용할 수 있다.
<T extends string | number>에서, T는 string 또는 number가 될 수 있는 이유는
T는 string | number의 부분집합이여야 하기 때문이다.

extend vs intersection

다음과 같이 interface의 extend를 할때, 같은 key값을 가지는 property가 있는 경우 conflict error가 발생한다.

interface NumberToStringConverter {
  convert: (value: number) => string;
}

interface BidirectionalStringNumberConverter extends NumberToStringConverter {
  convert: (value: string) => number;
}
error TS2430: Interface 'BidirectionalStringNumberConverter' incorrectly extends interface 'NumberToStringConverter'.

그러나 intersection은 두 property를 combine한다

type NumberToStringConverter = {
  convert: (value: number) => string;
}

type BidirectionalStringNumberConverter = NumberToStringConverter & {
  convert: (value: string) => number;
}
const converter: BidirectionalStringNumberConverter = {
    convert: (value: string | number) => {
        return (typeof value === 'string' ? Number(value) : String(value)) as string & number; // type assertion is an unfortunately necessary hack.

key of type

key of operator는 object의 key를 literal union으로 만들어준다.

type Person = {
  name : string,
  age : number
}
type A = keyof Person
///A = 'name' | 'age'

index signature에서 keyof operator를 사용할 경우 number이 자동적으로 포함된다.
obj[0] 으로 접근하는 것은 obj["0"]과 동일하기 때문이다.

type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
    
type A = number
 
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
    
type M = string | number

mapped type

in key of를 사용하여 readonly를 속성을 제거하는 예제

type CreateMutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};
 
type LockedAccount = {
  readonly id: string;
  readonly name: string;
};
 
type UnlockedAccount = CreateMutable<LockedAccount>;
           
type UnlockedAccount = {
    id: string;
    name: string;
}

mapped type을 이용해서 optional 속성을 제거하거나, exclude와 함께 사용하여 특정 property만들 제거한 type을 생성하거나, conditional type와 함께 사용할 수도 있다.

object => enum

keyof typeof 를 사용해서 object를 enum처럼 사용할 수 있다.

const enum EDirection {
  Up,
  Down,
  Left,
  Right,
}
 
const ODirection = {
  Up: 0,
  Down: 1,
  Left: 2,
  Right: 3,
} as const;
 
EDirection.Up;
           
(enum member) EDirection.Up = 0
 
ODirection.Up;
           
(property) Up: 0
 
// Using the enum as a parameter
function walk(dir: EDirection) {}
 
// It requires an extra line to pull out the keys
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
 
walk(EDirection.Left);
run(ODirection.Right);

Custom Type Guard

함수의 return type으로 arg is {type}을 주게되면, Type Guard로 인식한다.

function getWheelOrMotor(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';
}

Utility Types

Partial <T>

T의 모든 property를 optional로 변경

type Partial<T> = {
  [P in keyof T]? : T[P];
};

Pick<T,K>

T의 key 중에서, K에 해당하는 속성을 선택하여 반환

type Pick<T , K extends keyof T> = {
  [P in K] : T[P];
};

Extract<T,U>

Union T에서 U 와 겹치는 부분을 추출

type Extract<T,U> = T extends U ? T : never;

NonNullable<T>

Union T에서 null과 undefined를 제외한 타입을 반환

type NonNullable<T> = T extneds null | undefined ? never : T;

Parameters<T>

함수 T의 매개변수 타입을 배열로 반환

type Parameters<T extends (...args : any) => any> = T extends (...args : infer P) => any ? P : never;

ReturnType<T>

함수 T의 return 타입을 반환

type ReturnType<T extends (...args : any) =>any> = T extends (...args : any) => infer R ? R : any;

공변성과 반공변성

공변성 : A가 B의 서브타입이면, T<A>는 T<B>의 서브타입이다.
반공변성 : A가 B의 서브타입이면, T<B>는 T<A>의 서브타입이다.
이변성 : A가 B의 서브타입이면, T<A> -> T<B>도 되고, T<B> -> T<A>도 되는경우

타입스크립트에서는 일반적으로 공변성이 적용된다.

let stringArray : Array<string> = [];
let array : Array<string | number | boolean> = [];

array = stringArray   /// 가능
stringArray = array   /// 불가능

string은 string | number | boolean의 서브타입이므로, Array<string> 은 Array<string | number | boolean>의 서브타입임이 성립하는 경우와 같은 경우를 공변적이라고 할 수 있다.

그러나 tsconfig에서 strictFunctionTypes가 true일때 함수의 매개변수타입은 반공변적이 된다.

type A = (x : string | number) => number;
type B = (x : string) => number;

function a(x: string | number) : number {
  return 0;
}
function b(x : string) : number{
  return 0;
}

let bb : B = a; //가능
let aa : A = b; //불가능

매개변수로 string만을 처리하는 함수에 string | number을 처리할수 있는 함수를 대입하는 것은 가능하지만, string | number을 처리할 수 있는 함수에 string만을 처리하는 함수를 대입하는 것은 부족하기 때문에 반공변성이 적용된다.

또한, strictFunctionTypes가 false일때 함수의 매개변수 타입은 이변적이된다.
이변성을 가질때는 함수끼리 매개변수가 달라도 서로 대입이 가능하다.

출처

0개의 댓글