Woongjae Lee
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team
Lead Software Engineer @ProtoPie
Microsoft MVP
TypeScript Korea User Group Organizer
Marktube (Youtube)
이 웅재
git clone https://github.com/xid-mark/lgcns.gitcd lgcnsnvm usenpm cinpx tsc --versionnpm run build:watch/*
아래 변수는 "Hello" 라는 문자열, 2020 이라는 숫자, true 라는 boolean 값을 가질 수 있습니다.
이 3 가지를 만족하는 가장 제한된 타입 A 를 만들어서 변수에 설정하세요.
*/
let a = "Hello";
a = 2020;
a = true;
type A = "Hello" | 2020 | true;
let a: A = "Hello";
a = 2020;
a = true;
/*
아래 변수 b 를 모두 만족하는 타입 B 를 만들어서 변수에 설정하세요.
b 는 name 을 필수로 가집니다. name 은 문자열입니다.
b 는 name 이외의 모든 프로퍼티를 사용 가능합니다. 해당 프로퍼티는 숫자거나 문자열 입니다.
*/
let b = {
  name: "Mark",
};
b = {
  name: "Mark",
  age: 38,
};
b = {
  name: "Mark",
  country: "Korea",
};
interface StringArray {
    [index: number]: string;
}
const sa: StringArray = {}; // 옵셔널하다
sa[100] = '백';
interface StringDictionary {
    [index: string]: string;
}
const sd: StringDictionary = {}; // 옵셔널하다
sd.hundred = '백';
interface StringArrayDictionary {
    [index: number]: string;
    [index: string]: string;
}
const sad: StringArrayDictionary = {};
// 당연히 옵셔널하다.
sad[100] = '백';
sad.hundred = '백';interface StringDictionary {
    [index: string]: string;
    name: string;
}
const sd: StringDictionary = {
    name: '이름' // 필수
};
sd.any = 'any'; // 어떤 프로퍼티도 가능
////////////////////////////////////////////////
interface StringDictionaryNo {
    [index: string]: string;
    // name: number; // (X) 인덱서블 타입이 string 값을 가지기 때문에 number 를 필수로 끌어오면 에러
}interface B {
  name: string;
  [index: string]: number | string;
}
let b: B = {
  name: "Mark",
};
b = {
  name: "Mark",
  age: 38,
};
b = {
  name: "Mark",
  country: "Korea",
};
/*
아래 함수 c 는 2가지 형태로 사용이 가능합니다.
첫번째는 문자열을 받아 숫자를 반환하는 것이고,
두번째는 숫자와 숫자 배열을 받아 숫자를 반환하는 것입니다.
c1 과 c2 로 구현했습니다.
c 라는 함수가 두가지 함수를 사용 가능하도록 구현하세요. (힌트 : 오버로딩)
*/
function c1(str: string): number {
  return str.length;
}
function c2(num: number, numArr: number[]): number {
  return num + numArr.reduce((acc, cur) => acc + cur, 0);
}
function shuffle(value: string | any[]): string | any[] {
  if (typeof value === 'string')
    return value
      .split('')
      .sort(() => Math.random() - 0.5)
      .join('');
  return value.sort(() => Math.random() - 0.5);
}
console.log(shuffle('Hello, Mark!')); // string | any[]
console.log(shuffle(['Hello', 'Mark', 'long', 'time', 'no', 'see'])); // string | any[]
console.log(shuffle([1, 2, 3, 4, 5])); // string | any[]
function shuffle2<T extends string | any[]>(
  value: T,
): T extends string ? string : T;
function shuffle2(value: any) {
  if (typeof value === 'string')
    return value
      .split('')
      .sort(() => Math.random() - 0.5)
      .join('');
  return value.sort(() => Math.random() - 0.5);
}
// function shuffle2<"Hello, Mark!">(value: "Hello, Mark!"): string
shuffle2('Hello, Mark!');
// function shuffle2<string[]>(value: string[]): string[]
shuffle2(['Hello', 'Mark', 'long', 'time', 'no', 'see']);
// function shuffle2<number[]>(value: number[]): number[]
shuffle2([1, 2, 3, 4, 5]);
// error! Argument of type 'number' is not assignable to parameter of type 'string | any[]'.
shuffle2(1);
function shuffle3(value: string): string;
function shuffle3<T>(value: T[]): T[];
function shuffle3(value: string | any[]): string | any[] {
  if (typeof value === 'string')
    return value
      .split('')
      .sort(() => Math.random() - 0.5)
      .join('');
  return value.sort(() => Math.random() - 0.5);
}
shuffle3('Hello, Mark!');
shuffle3(['Hello', 'Mark', 'long', 'time', 'no', 'see']);
shuffle3([1, 2, 3, 4, 5]);function c1(str: string): number {
  return str.length;
}
function c2(num: number, numArr: number[]): number {
  return num + numArr.reduce((acc, cur) => acc + cur, 0);
}
function c(str: string): number;
function c(num: number, numArr: number[]): number;
function c(strOrNum: string | number, numArr?: number[]): number {...}
function c(strOrNum: string | number, numArr?: number[]): number {
  if (typeof strOrNum === "string") {
    return c1(strOrNum);
  } else {
    return c2(strOrNum, numArr!);
  }
}
function c(str: string): number;
function c(num: number, numArr: number[]): number;
function c(strOrNum: string | number, numArr?: number[]): number {
  if (typeof strOrNum === "string") {
    return strOrNum.length;
  } else {
    return strOrNum + numArr!.reduce((acc, cur) => acc + cur, 0);
  }
}/*
아래 조건을 만족하는 ICar 인터페이스와 Volvo 클래스를 작성하세요.
- 해당 조건을 제외하고는 사용할 수 없도록 컴파일 에러를 발생시키는 것에 주의해야 합니다.
- 아래 예시 중 주석 부분은 로그로 출력되는 내용입니다. 일치하도록 로직을 작성하세요.
- 아래 예시 중 [컴파일 에러!!] 가 나는 경우는 해당 동작을 할 수 없도록 해야합니다.
- 필요한 경우, 추가로 타입을 만들어도 좋습니다.
*/
interface ICar {}
class Volvo implements ICar {}
const myCar: ICar = new Volvo("suv");
console.log(myCar.type, myCar.isStarted); // 'suv' false
myCar.open("lf"); // '좌측 앞쪽 문이 열린다.'
console.log(myCar.doorOpenState); // { lf: true, rf: false, lb: false, rb: false }
myCar.doorOpenState.lf = false; // [컴파일 에러!!]
myCar.closeAllDoors();
console.log(myCar.doorOpenState); // { lf: false, rf: false, lb: false, rb: false }
myCar.start(); // '차가 출발한다.'
console.log(myCar.isStarted); // true
interface ICar {
  type: string;
  readonly isStarted: boolean;
  start(): void;
  open(where: "lf" | "rf" | "lb" | "rb"): void;
  readonly doorOpenState: DoorsOpenState;
  closeAllDoors(): void;
}interface DoorsOpenState {
  readonly lf: boolean;
  readonly rf: boolean;
  readonly lb: boolean;
  readonly rb: boolean;
}
interface ICar {
  type: string;
  readonly isStarted: boolean;
  start(): void;
  open(where: keyof DoorsOpenState): void;
  readonly doorOpenState: DoorsOpenState;
  closeAllDoors(): void;
}class Volvo implements ICar {
  private _isStarted = false;
  private _doorsOpenState = {
    lf: false,
    rf: false,
    lb: false,
    rb: false,
  };
  constructor(public type: string) {}
  public start(): void {
    this._isStarted = true;
    console.log("차가 출발한다.");
  }
  public open(where: keyof DoorsOpenState): void {
    console.log("좌측 앞쪽 문이 열린다.");
  }
  get doorOpenState() {
    return this._doorsOpenState;
  }
  get isStarted() {
    return this._isStarted;
  }
  public closeAllDoors(): void {
    this._doorsOpenState = {
      lf: false,
      rf: false,
      lb: false,
      rb: false,
    };
  }
}/*
Toast 라는 타입은 모두 type 이라는 프로퍼티를 가지고 type 은 다음과 같은 것으로 분류할 수 있다.
"AFTER_SAVED" | "AFTER_PUBLISHED" | "AFTER_RESTORE"
*/
enum ToastType {
  AFTER_SAVED,
  AFTER_PUBLISHED,
  AFTER_RESTORE,
}
interface Toast {
  type: ToastType;
  createdAt: string;
}
const toasts: Toast[] = [];
/*
toasts 안에 여러개의 toast 가 있습니다.
toasts 를 인자로 받아서 각각의 toast 를 문자열로 변환하는 함수를 작성합니다.
각각의 toast 를 문자열로 바꿔 문자열 배열을 리턴하는 것으로 가정하고, 아래의 로직을 작성하세요.
- toasts 를 돌며 toast 를 꺼내서 toast 의 타입이 AFTER_SAVED 인 경우,
"저장 된 직후에 뜬다!!" 라고 문자열을 만들어냅니다.
- toasts 를 돌며 toast 를 꺼내서 toast 의 타입이 AFTER_PUBLISHED 인 경우,
"퍼블리시가 된 직후에 뜬다!!" 라고 문자열을 만들어냅니다.
- toasts 를 돌며 toast 를 꺼내서 toast 의 타입이 AFTER_RESTORE 인 경우,
"복원 된 직후에 뜬다!!" 라고 문자열을 만들어냅니다.
해당 함수를 안전하게 완성해주세요 (ToastType 이 추가된 경우에도 안전하도록 작성해보세요.)
*/
function convert(toasts) {}
enum ToastType {
    AFTER_SAVED,
    AFTER_PUBLISHED,
    AFTER_RESTORE,
}
interface Toast {
    type: ToastType,
    createdAt: string,
}
const toasts: Toast[] = [...];// toastNodes1 -> (JSX.Element | undefined)[]
const toastNodes1 = toasts.map((toast) => {
  if (toast.type === ToastType.AFTER_SAVED)
    return (
      <div key={toast.createdAt}>
        <AfterSavedToast />
      </div>
    );
  else if (toast.type === ToastType.AFTER_PUBLISHED)
    return (
      <div key={toast.createdAt}>
        <AfterPublishedToast />
      </div>
    );
  else if (toast.type === ToastType.AFTER_RESTORE)
    return (
      <div key={toast.createdAt}>
        <AfterRestoredToast />
      </div>
    );
});// toastNodes2 -> JSX.Element[]
const toastNodes2 = toasts.map((toast) => {
  if (toast.type === ToastType.AFTER_SAVED)
    return (
      <div key={toast.createdAt}>
        <AfterSavedToast />
      </div>
    );
  else if (toast.type === ToastType.AFTER_PUBLISHED)
    return (
      <div key={toast.createdAt}>
        <AfterPublishedToast />
      </div>
    );
  else
    return (
      <div key={toast.createdAt}>
        <AfterRestoredToast />
      </div>
    );
});// toastNodes3 -> JSX.Element[]
const toastNodes3 = toasts.map((toast) => {
  if (toast.type === ToastType.AFTER_SAVED)
    return (
      <div key={toast.createdAt}>
        <AfterSavedToast />
      </div>
    );
  else if (toast.type === ToastType.AFTER_PUBLISHED)
    return (
      <div key={toast.createdAt}>
        <AfterPublishedToast />
      </div>
    );
  else if (toast.type === ToastType.AFTER_RESTORE)
    return (
      <div key={toast.createdAt}>
        <AfterRestoredToast />
      </div>
    );
  else return neverExpected(toast.typs);
});
function neverExpected(value: never): never {
  throw new Error(`Unexpected value : ${value}`);
}// toastNodes4 -> JSX.Element[]
const toastNodes4 = toasts.map((toast) => {
  if (toast.type === ToastType.AFTER_SAVED)
    return (
      <div key={toast.createdAt}>
        <AfterSavedToast />
      </div>
    );
  if (toast.type === ToastType.AFTER_PUBLISHED)
    return (
      <div key={toast.createdAt}>
        <AfterPublishedToast />
      </div>
    );
  if (toast.type === ToastType.AFTER_RESTORE)
    return (
      <div key={toast.createdAt}>
        <AfterRestoredToast />
      </div>
    );
  return neverExpected(toast.typs);
});const toastNodes5 = toasts.map((toast) => {
  switch (toast.type) {
    case ToastType.AFTER_SAVED:
      return (
        <div key={toast.createdAt}>
          <AfterSavedToast />
        </div>
      );
    case ToastType.AFTER_PUBLISHED:
      return (
        <div key={toast.createdAt}>
          <AfterPublishedToast />
        </div>
      );
    case ToastType.AFTER_RESTORE:
      return (
        <div key={toast.createdAt}>
          <AfterRestoredToast />
        </div>
      );
    default:
      return neverExpected(toast.type);
  }
});
function convert(toasts: Toast[]) {
  return toasts.map((toast) => {
    switch (toast.type) {
      case ToastType.AFTER_SAVED:
        return "저장 된 직후에 뜬다!!";
      case ToastType.AFTER_PUBLISHED:
        return "퍼블리시가 된 직후에 뜬다!!";
      case ToastType.AFTER_RESTORE:
        return "복원 된 직후에 뜬다!!";
      default:
        return neverExpected(toast.type);
    }
  });
}
function neverExpected(value: never): never {
  throw new Error(`Unexpected value : ${value}`);
}/*
class 의 method 에 decorator 를 사용해서 해당 class 의 this 를 자동으로 바인딩할 수 있습니다.
아래의 Timer 는 new Timer().start(); 를 이용해서 실행하면, NaN 이 출력됩니다.
_loop 메서드의 위에 @autobind 를 달아 정상적으로 출력되도록 autobind 함수를 작성하세요.
*/
function autobind() {}
class Timer {
  private _count = 0;
  private _timer: number | null = null;
  public start() {
    this._timer = setInterval(this._loop, 1000);
  }
  public stop() {
    if (this._timer !== null) {
      clearInterval(this._timer);
      this._count = 0;
    }
  }
  // @autobind
  private _loop() {
    console.log(this._count++);
  }
}
new Timer().start();
function methodDecorator(
	target: any,
	propertyKey: string,
	descriptor: PropertyDescriptor
) {...}
function methodDecoratorFactory(...) {
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {...};
}function methodDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  console.log('target', target);
  console.log('propertyKey', propertyKey);
  console.log('descriptor', descriptor);
}
class Test3 {
  @methodDecorator
  public print() {}
}
// target Test3 { print: [Function] }
// propertyKey print
// descriptor { value: [Function], writable: true, enumerable: true, configurable: true }target
클래스
propertyKey
메소드 이름
descriptor
PropertyDescriptor (lib.es5.d.ts)
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
interface PropertyDescriptor {
    configurable?: boolean;
    enumerable?: boolean;
    value?: any;
    writable?: boolean;
    get?(): any;
    set?(v: any): void;
}
	configurable
이 속성기술자는 해당 객체로부터 그 속성을 제거할 수 있는지를 기술한다.
true 라면 삭제할 수 있다. 기본값은 false.
enumerable
해당 객체의 키가 열거 가능한지를 기술한다.
true 라면 열거가능하다. 기본값은 false.
value
속성에 해당되는 값으로 오직 적합한 자바스크립트 값 (number, object, function, etc) 만 올 수 있다.
	기본값은 undefined.
writable
writable이 true로 설정되면 할당연산자 assignment operator 를 통해 값을 바꿀 수 있다.
	기본값은 false.
function log(show: boolean = true) {
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;
    descriptor.value = function(...args: any[]) {
      show && console.log('start');
      original(...args);
      show && console.log('end');
    };
  };
}
class Test5 {
  @log()
  print1(first: string, second: number) {
    console.log('print1', first, second);
  }
  @log(false)
  print2(first: string, second: number) {
    console.log('print2', first, second);
  }
}
const test5 = new Test5();
test5.print1('mark', 36);
test5.print2('mark', 36);class Timer {
  private _count = 0;
  private _timer: number | null = null;
  public start() {
    this._timer = setInterval(this._loop, 1000);
  }
  public stop() {
    if (this._timer !== null) {
      clearInterval(this._timer);
      this._count = 0;
    }
  }
  @autobind
  private _loop() {
    console.log(this._count++);
  }
}function autobind(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const fn = descriptor.value;
  return {
    get() {
      const boundFn = fn.bind(this);
      return boundFn;
    },
  };
}/*
state 는 읽을 수만 있고, 변경을 하려면 새로 데이터를 만들어야 하는 전형적인 immutable data 입니다.
아래 상태에서는 내부 데이터가 변경이 가능합니다.
예를 들어, state1.books[0].title = "새 책 이름"; 과 같이 변경이 가능합니다.
컴파일 타임에 변경이 불가능하도록 DeepReadonly<T> 라는 타입을 만들어보세요.
(어떠한 State 에도 대응이 가능하도록 범용적인 타입을 만들어보세요.)
*/
const state = {
  books: [{ title: "책 이름", author: "저자" }],
};
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'.
};
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'.
};type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;
const promises = [Promise.resolve('Mark'), Promise.resolve(38)];
type Expected = UnpackPromise<typeof promises>; // string | numberfunction plus1(seed: number): number {
  return seed + 1;
}
type MyReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;
type Id = MyReturnType<typeof plus1>;
lookupEntity(plus1(10));
function lookupEntity(id: Id) {
  // query DB for entity by ID
}
// type Exclude<T, U> = T extends U ? never : T;
type Excluded = Exclude<string | number, string>; // number - diff
// type Extract<T, U> = T extends U ? T : never;
type Extracted = Extract<string | number, string>; // string - filter
// Pick<T, Exclude<keyof T, K>>; (Mapped Type)
type Picked = Pick<{name: string, age: number}, 'name'>;
// type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Omited = Omit<{name: string, age: number}, 'name'>;
// type NonNullable<T> = T extends null | undefined ? never : T;
type NonNullabled = NonNullable<string | number | null | undefined>;/*
type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;
*/
/*
type Parameters<T extends (...args: any) => any> = T extends (
  ...args: infer P
) => any
  ? P
  : never;
*/
type MyParameters = Parameters<(name: string, age: number) => void>; // [name: string, age: number]
interface Constructor {
  new (name: string, age: number): string;
}
/*
type ConstructorParameters<
  T extends new (...args: any) => any
> = T extends new (...args: infer P) => any ? P : never;
*/
type MyConstructorParameters = ConstructorParameters<Constructor>; // [name: string, age: number]
/*
type InstanceType<T extends new (...args: any) => any> = T extends new (
  ...args: any
) => infer R
  ? R
  : any;
*/
type MyInstanceType = InstanceType<Constructor>; // string
type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface Person {
  id: number;
  name: string;
  hello(message: string): void;
}
type T1 = FunctionPropertyNames<Person>;
type T2 = NonFunctionPropertyNames<Person>;
type T3 = FunctionProperties<Person>;
type T4 = NonFunctionProperties<Person>;
interface IPerson {
  name: string;
  age: number;
}
type ReadonlyPerson = Readonly<IPerson>;
const person: ReadonlyPerson = Object.freeze<IPerson>({
  name: "Mark",
  age: 38,
});
person.name = "Hanna"; // error!
person.age = 27; // error!
interface IPerson {
  name: string;
  age: number;
}
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};
type Stringify<T> = {
  [P in keyof T]: string;
};
type PartialNullablePerson = Partial<Nullable<Stringify<IPerson>>>;
/*
type PartialNullablePerson = {
    name?: string | null | undefined;
    age?: string | null | undefined;
    speak?: string | null | undefined;
}
*/
let pnp: PartialNullablePerson;
pnp = { name: 'Mark', age: '38' };
pnp = { name: 'Mark' };
pnp = { name: undefined, age: null };// Make all properties in T optional
type Partial<T> = {
    [P in keyof T]?: T[P];
};
// Make all properties in T required
type Required<T> = {
    [P in keyof T]-?: T[P];
};
// Make all properties in T readonly
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};
// From T, pick a set of properties whose keys are in the union K
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
// Construct a type with a set of properties K of type T
type Record<K extends keyof any, T> = {
    [P in K]: T;
};interface Book {
  title: string;
  author: string;
}
interface IRootState {
  book: {
    books: Book[];
    loading: boolean;
    error: Error | null;
  };
}
type IReadonlyRootState = Readonly<IRootState>;
let state1: IReadonlyRootState = {} as IReadonlyRootState;
const book1 = state1.book.books[0];
book1.title = 'new';type DeepReadonly<T> = T extends (infer E)[]
  ? ReadonlyArray<DeepReadonlyObject<E>>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;
type DeepReadonlyObject<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> };
const state: DeepReadonly<{ books: { title: string; author: string }[] }> = {
  books: [{ title: "책 이름", author: "저자" }],
};
state.books[0].author = "마크";
By Woongjae Lee
LG CNS 타입스크립트 특강 4주차
Daangn - Frontend Core Team ex) NHN Dooray - Frontend Team Leader ex) ProtoPie - Studio Team