[TIL] <2020.05.21~28> Fun Times with Advanced TypeScript 정리

이성진·2020년 5월 28일
0

TIL

목록 보기
6/9

이 글은 유튜브 "Fun Times with Advanced TypeScript"의 영삼을 참고하였음을 알려드립니다.

또, 완전히 이해하고 적은 영상이 아니여서 뭐라하지 말아주세요..


Generic

Typescript Generic

함수에 어떠한 타입이 올 지 모를 때 <> 를 사용하여 타입을 지정합니다.
타입의 이름은 자유롭게 작성이 가능하며 관례상 대문자로 작성합니다.

Building Blocks

Intersection Types (여러 타입을 합쳐서 사용하는 타입) // 교집합?

type Duck = Quackable & Swimmable;

또는

interface User {
  name: string;
}
interface Action {
  do(): void;
}
function createUserAction(u: User, a: Action) : User & Action { // intersection types
  return { ...u, ...a };
}
const u = createUserAction({ name: "Lee" }, { do() {} });

Union type (변수 또는 함수 매개 변수에 둘 이상의 타입을 사용) // 합집합?

type Flayble = Eagle | Butterfly;

let strOrNum: string | number;
strOrNum = 1 // OK
strOrNum = "Lee" // OK

Type Guard

let pet = getSmallPet();

if ((<Fish>pet).swim) {
  (<Fish>.pet).swim();
} else {
  (<Bird>.pet).fly();
}

Using type predicats (타입 서술어 사용하기)

function isFish(pet: Fish | Bird): pet is Fish { 
  return (pet as Fish).swim !== undefined;
}
// pet is Fish : 타입 가드
// pet as Fish : 타입 단언

Using the in operator

in 연산자는 타입을 좁히는 표현으로 작용합니다.
n in x 표현에서 n문자열 리터럴 혹은 문자열 리터럴 타입
x유니온 타입

function move(pet: Fish | Bird) {
  if ("swim" in pet) {
  	// "swim" 을 필수 혹은 선택적으로 가지는 타입으로 좁힘
    return pet.swim(); // 있으면 swim
  }
  return pet.fly(); // 없으면 fly
}

typeof type guards

타입스크립트는 typeof 도 타입 가드로 인식하기 때문에 함수로 추상화 할 필요없이 인라인으로 검사할 수 있습니다.

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") { 
    // isNumber(x: any): x is number => typeof x === "number"; // OK
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") { 
    // isString(x: any): x is string => typeof x === "string"; // OK
    return padding + value;
  }
  throw new Error("Expected string or number, got `${padding}`.");
}

typeof 타입 가드의 형식인 typeof v === "typename"typeof v !== "typename"이 있습니다.
이 때 typename"number", "string", "boolean", "symbol" 이어야 합니다. TS에서 위에 없는 문자열과 비교하는 것을 막지는 않지만, 타입 가드의 표현식으로 인지하지 않습니다.

Custom Type Guards

const canfly = (animal: Animal): animal is Flyable
  => typeof (animal as any).fly === "function"; 
// animal의 타입이 any고 
// fly 메서드의 타입이 function 일 때 true를 반환합니다.

if (canFly(animal)) {
  animal.fly();
}

Type Assertion

person.children[0]!.name;
null 또는 undefined를 제거해준다고 하는데 아직까지 정확한 활용법을 모르겠다.

구체적인 예시로

let name: (string | null);
name.charAt(0) // Error, 'name'이 null 일 수 있기 때문
name = name || "Lee" // 여기선 "Lee" 로 바뀜

// 그래서 아래와 같이 느낌표(type assertion, 타입 단언)을 넣어주면 됨
name!.charAt(0) // Good

// 확정적 타입 단언
// 확정적 타입 단언은 프로퍼티의 인스턴스를 !를 이용해 사용할 수 있게해주는 것? 정확하게는 모르겠다.

Type Inference

let bool = true;
const arr = [1, 2, 3];
const tuple = [true, 1];
bool = 1; // Error!

위의 예제에서 각 변수의 타입은 적절하게 추론되므로 다시 사용할 때 Type Safe 하게 사용할 수 있다.
bool의 타입은 boolean으로 추론되므로 number 타입인 1을 할당하려고 하면 에러가 발생한다.
arr 변수는 배열에 숫자만 담겨져 있는 걸로 봐서 number[]로 추론된다. 이 때 배열의 길이는 고정이 아닌 것으로 추론한다.
아래의 tuple도 마찬가지로 배열로 추론된다. 튜플로 사용하고 싶다면 별 수 없이 타입을 선언해주어야 한다. 다만 배열의 요소가 각각 boolean과 number 이기에 최종적으로 추론되는 타입은 boolean과 number의 유니온 타입의 배열, (boolean | number)이다.

여기서 배열에서 사용된 요소들의 타입을 각각 추론하여 유니온 타입으로 만들어 내는 방식을 Best common type 이라고 부른다.

Best common type

말 그대로 가장 일반적인 타입.
여러 가지 타입이 사용될 때 그 타입을 포괄할 수 있는 가장 일반적인 타입을 추론하는 것이다.
위의 예제에서는 true와 1을 포괄할 수 있는 타입인 (boolean | number)가 추론된 것.

Literal Types

// 스트링 자체를 타입으로 사용할 수 있다.
type Move = "ROCK" | "PAPER" | "SICSSOR";

Never

const throws = (): never => {
  throw new Error("This never returns"); // 리턴값이 절대 없음
};

const loops = (): never => {
  while (true) {
    console.log("This never returns either"); // 예외가 절대 없음
  }
};

Unknown

let a: string;

let x: any;
a = x; // Compiles
x = a; // Compiles

let y: unkown;
y = a; // Compiles
a = y; // Does net compile
// unknown 타입은 any 타입과 마찬가지로 모든 타입이 할당 될 수 있다.
// 그러나, unknown 타입으로 선언된 변수는 any를 제외한 다른 타입으로 선언된 변수에 할당될 수 없다.

// 또 프로퍼티에 접근 불가능
y.foo.bar // Error: Object is of type 'unknown'.
y.method() // Error
y() // Error
new y() // Error

// 타입 가드와 함께라면 접근 가능하다.
let variable: unknown
declare function isFunction(x: unknown): x is Function

if (isFunction(variable)) {
  variable(); // OK
}

Index Types

인덱스로 접근하는 타입

type Duck = {
  colors: string;
  feathers: number;
}

type DuckProps = keyof Duck; // = 'colors' | 'feathers'
type colorType = Duck['colors']; // = string
type DuckValues = Duck[DuckProps] // string | number

Conditional Types

조건을 이용하여 타입을 지정하는 것

type StringOrNumber<T> = T extends boolean ? string : number;

type T1 = StringOrNumber<true> // string
type T2 = StringOrNumber<false> // string
type T3 = StringOrNumber<Object> // number

// In application

type TypeName<T> = 
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T0 = TypeName<string> // "string"

Infer

type ElementType<T> = T extends (infer U)[] ? U : never;
type T = ElementType<[]> // = never
type T1 = ElementType<string[]> // = string

// 만약 T가 '어떤 것'의 배열이면 타입은 '어떤 것'
// 그렇지 않으면 타입은 'never'

Misc Patters

Functional Programming

interface RawPerson {
  identifier: number;
  first_name: string;
  last_name: string;
}
interface Person {
  id: strin;
  fullName: string;
}
const transformPerson = (raw: RawPerson): Person => {
  return {
    id: `${raw.identifier}`,
    fullName: `${raw.first_name} ${raw.last_name}`,
  }
}

Discriminate Unions

서로 다른 값과 형식을 가진 몇몇 사례 중 하나 일 수 있는 값을 말합니다.
약간 reducer같다고 저는 생각했습니다.

type Eagle = {
  kind: "eagle";
  fly: () => "fly";
};
type Duck = {
  kind: "duck";
  quack: () => "quack";
};
type Bird = {
  kind: "bird";
};

type Animal = Eagle | Duck | Bird;

const assertNever = (animal: Animal): never => {
  throw new Error(`Unknown animal ${animal.kind}`);
}

const doSomething = (animal: Animal): string => {
  switch(animal.kind) {
    case "eagle":
      return animal.fly();
    case "duck":
      return animal.quack();
    default:
      return assertNever(animal);
  }
};

Derive Types From Constants

뭔지 잘 모르겠네요.. ㅜㅜ

const MOVES = {
  ROCK: { beats: "SCISSOR" },
  PAPER: { beats: "ROCK" },
  SCISSOR: { beats: "PAPER" },
};
type Move = keyof typeof MOVES;
const move: Move = "ROCK";

Untrusted User Input

// 유저의 인풋을 믿지 못할 때 사용하는 타입?
const validateInt = (s: unknown): number => {
  let n;
  switch (typeof s) {
    case "number":
      // handle
    case "string":
      // handle
    default:
      throw new Error("Not a number.");
  }
}

Mapped Types

Readonly

읽기 전용으로 만들 때

type Readonly<T> = { readonly [P in keyof T]: T[P] };
type ReadonlyDuck = Readonly<Duck>;
// = { readonly color: string; readonly feathers: number }

Partial

부분 조건으로 만들 때

type Partial<T> = { [P in keyof T]?: T[P] };
type PartialDuck = Partial<Duck>;
// = { color?: string; feathers?: number }

Required

필수 조건으로 만들 때

type PartialDuck = {
  color?: string;
  feathers?: number;
}
type Required<T> = { [P in keyof T]-?: T[P] }; // - (required)
type Duck = Required<PartialDuck>;

Nullable

type Nullable<T> = { [P in keyof T]: T[P] | null };

Pick

type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type ColorDuck = Pick<Duck, "color">;
// = { color: string }

Pick SQL

아직 이해하지 못하는거.
재네릭을 겹치는 것.
Promise<Pick<Reaction , T>> 과 같이 제네릭이 겹칠 때
그 프로세스를 모르겠음.
약간 흐름이 어떻게 가는지...

async fucntion fetchPersonById<T extends keyof Person> (
  id: string,
  ...fields: T[]
): Promise<Pick<Reaction, T>> {
  return awiat knex("Person")
  	.where({ id })
	.select(fields)
	.first();
}

const reaction = awiat fetchPersonById(id, "name", "age");
// = { name: string, age: number }

Record

이해 못함

type Record<K extends string, T> = { [P in K]: T };
type Day = "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun";
type EverydayParty = Record<Day>; 

Filtering Union Types

Filter / Extract

type Filter<T, U> = T extends U ? T : never;
type T1 = Filter<"a" | "b", "a"> // = "a"
// Native code 같음
type T2 = Extract<"a" | "b", "a"> // = "a"

Diff / Exclude

type Diff<T, U> = T extends U ? never : T;
type T1 = Diff<"a" | "b", "a"> // = "b"
// Native code 같음
type T2 = Exclude<"a" | "b", "a"> // = "b"

Nonnullable

type NonNullable<T> = Diff<T, null | undefined>; 
// Diff -> T 로 받은 타입이 null 또는 undefined 의 확장된 것인가?
// 확장된 것이면 -> return never
// 확장된 것이 아니면 -> return T
type T = NonNullable<string | null | undefined>;
// 따라서 string은 null 또는 undefined 의 확장된 것이 아니기 때문에 string
// = string

Omit

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type partialDuck = Omit<Duck, "feathers">;
// = { color: string }

React HOCs (High Order Componnts)

export interface WithUserProps {
  user: User;
}
export const withUser = <P extends WithUserProps>
  (Component: React.ComponentType<P>) => (props: Omit<P, WithUserProps>) => {
  	<Component {...props} user={getUser()} />
  }
const UsernameComponent = ({ user, message }: { user: User, message: string }) => (
  <div>Hi {user.username}! {message}</div>
}

const Username = withUser(UsernameComponent); // ({ message }) => JSX.Element

Conditional and Mapped Types

Function Properties

type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never
}[keyof T];

type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

type Duck = {
  color: string, 
  fly: () => void
}
type T1 = FunctionProperties<Duck>;
// = { fly: () => void }
type T2 = FunctionPropertyNames<Duck>; // = "fly"

Nonfunction Properties

type NonFunctionPropertyNames<T> = {
  [k in keyof T]: T[K] extends Function ? never : K
}[keyof T]

type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNams<T>>;

type Duck = {
  color: string,
  fly: () => void,
}
type T1 = NonFunctionProperties<Duck>; // = { color: string }
type T2 = NonFunctionPropertyNames<Duck>; // = { color }

Return Type

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

const x = (a: string): number => 1;
type R = ReturnType<typeof x>; // = number

Promisified Redis

import { ClientOpts, createClient } from 'redis';
import { promisify } from 'util';

export const promisifyRedis = (opts: ClientOpts) => {
  const redis = createClient(opts);
  
  const promisifiedRedis = {
    setx: promisify(redis.setex),
    get: promisify(redis.get),
  };
  const wrappedRedis: typeof promisifiedRedis = { } as any;
  for (const key of Object.keys(promisifiedRedis) as (keyof typeof promisifiedRedis)[]) {
    wrappedRedis[key] = promisifiedRedis[key].bind(redis);
  }
  return wrappedRedis;
};

export type Redis = ReturnType<typeof promisifiedRedis>;
/* =
type Redis = {
  setex: (arg1: string, arg2: number, arg3: string) => Promise(string);
  get: (arg1: string) => Promise<...>;
}
*/

Instance Type

type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;

class Duck { }

const make2 = <T extends new (...args: any[]) => any>
  (constructor: T): [InstanceType<T>, InstanceType<T>] =>
    [new constructor(), new constructor()]

const ducks = make2(Duck); // = [Duck, Duck]

고급 타입 을 보고 느낌점

너무 어렵다. 참고로 나는 타스를 한 지 별로 안된 신생아 수준이기 때문에 여러 키워드에서 애를 먹고 있다. ex) extends, keyof, is, as, forin 등..

열심히 하자

글에 오류가 있으면 댓글 달아주세요.


References

TypeScript intersection & union Types
Fun Times with Advanced TypeScript
Unknown Type

profile
개발보다 회사 매출에 영향력을 주는 개발자

0개의 댓글