typescript exercise 풀이

HR·2023년 2월 16일
1

공식 홈페이지에 따르면, 이 exercise가 포함하는 범위는 아래와 같다.

  • Basic typing.
  • Refining types.
  • Union types.
  • Merged types.
  • Generics.
  • Type declarations.
  • Module augmentation.
  • Advanced type mapping.

1번

export type User = {
    name: string;
    age: number;
    occupation: string;
};

export const users: User[] = [
    {
        name: 'Max Mustermann',
        age: 25,
        occupation: 'Chimney sweep'
    },
    {
        name: 'Kate Müller',
        age: 23,
        occupation: 'Astronaut'
    }
];

export function logPerson(user: User) {
    console.log(` - ${user.name}, ${user.age}`);
}

관련 개념

Object Types

interface User {
    name: string;
    age: number;
    occupation: string;
};

const users: User[] = [
    {
        name: 'Max Mustermann',
        age: 25,
        occupation: 'Chimney sweep'
    },
    {
        name: 'Kate Müller',
        age: 23,
        occupation: 'Astronaut'
    }
];

export function logPerson(user: User) {
    console.log(` - ${user.name}, ${user.age}`);
}

2번

interface User {
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    name: string;
    age: number;
    role: string;
}

export type Person = User | Admin;

export const persons: Person[] = [
    {
        name: 'Max Mustermann',
        age: 25,
        occupation: 'Chimney sweep'
    },
    {
        name: 'Jane Doe',
        age: 32,
        role: 'Administrator'
    },
    {
        name: 'Kate Müller',
        age: 23,
        occupation: 'Astronaut'
    },
    {
        name: 'Bruce Willis',
        age: 64,
        role: 'World saver'
    }
];

export function logPerson(user: Person) {
    console.log(` - ${user.name}, ${user.age}`);
}

관련 개념

Union Types

interface User {
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    name: string;
    age: number;
    role: string;
}

export type Person = User | Admin;

3번

interface User {
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    name: string;
    age: number;
    role: string;
}

export type Person = User | Admin;

export const persons: Person[] = [
    {
        name: 'Max Mustermann',
        age: 25,
        occupation: 'Chimney sweep'
    },
    {
        name: 'Jane Doe',
        age: 32,
        role: 'Administrator'
    },
    {
        name: 'Kate Müller',
        age: 23,
        occupation: 'Astronaut'
    },
    {
        name: 'Bruce Willis',
        age: 64,
        role: 'World saver'
    }
];

export function logPerson(person: Person) {
    let additionalInformation: string;
    if ("role" in person) {
        additionalInformation = person.role;
    } else {
        additionalInformation = person.occupation;
    }
    console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}

관련 개념

Narrowing types with in
보통의 유니온 타입을 narrowing 하는 데에는 in이 사용된다.

interface User {
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    name: string;
    age: number;
    role: string;
}

export type Person = User | Admin;

//person 안에 role이라는게 있는지 확인
if ('role' in person) {
  additionalInformation = person.role;
} else {
  additionalInformation = person.occupation;
}

Narrowing types with instanceof
생성자 함수를 통해 생성된 인스턴스(ex. new 키워드)의 타입을 narrowing 하는 데에 사용된다.

function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString()); // x: Date
  } else {
    console.log(x.toUpperCase()); // x: string
  }
}

4번

interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';
    name: string;
    age: number;
    role: string;
}

export type Person = User | Admin;

export const persons: Person[] = [
    { type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
    { type: 'admin', name: 'Jane Doe', age: 32, role: 'Administrator' },
    { type: 'user', name: 'Kate Müller', age: 23, occupation: 'Astronaut' },
    { type: 'admin', name: 'Bruce Willis', age: 64, role: 'World saver' }
];

export function isAdmin(person: Person): person is Admin {
    return person.type === 'admin';
}

export function isUser(person: Person): person is User {
    return person.type === 'user';
}

export function logPerson(person: Person) {
    let additionalInformation: string = '';
    if (isAdmin(person)) {
        additionalInformation = person.role;
    }
    if (isUser(person)) {
        additionalInformation = person.occupation;
    }
    console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}

console.log('Admins:');
persons.filter(isAdmin).forEach(logPerson);

관련 개념

Type predicate in return

export function isAdmin(person: Person): person is Admin {
    return person.type === 'admin';
}

export function logPerson(person: Person) {
    let additionalInformation: string = '';
  
    if (isAdmin(person)) {
        additionalInformation = person.role;
    }
}

5번 문제

첫 번째 풀이 - partial 이용

interface User {
    type: 'user';
    name: string;
    age: number;
    occupation: string;
}

interface Admin {
    type: 'admin';
    name: string;
    age: number;
    role: string;
}

export type Person = User | Admin;

export const persons: Person[] = [
    { type: 'user', name: 'Max Mustermann', age: 25, occupation: 'Chimney sweep' },
    {
        type: 'admin',
        name: 'Jane Doe',
        age: 32,
        role: 'Administrator'
    },
    {
        type: 'user',
        name: 'Kate Müller',
        age: 23,
        occupation: 'Astronaut'
    },
    {
        type: 'admin',
        name: 'Bruce Willis',
        age: 64,
        role: 'World saver'
    },
    {
        type: 'user',
        name: 'Wilson',
        age: 23,
        occupation: 'Ball'
    },
    {
        type: 'admin',
        name: 'Agent Smith',
        age: 23,
        role: 'Administrator'
    }
];

export const isAdmin = (person: Person): person is Admin => person.type === 'admin';
export const isUser = (person: Person): person is User => person.type === 'user';

export function logPerson(person: Person) {
    let additionalInformation = '';
    if (isAdmin(person)) {
        additionalInformation = person.role;
    }
    if (isUser(person)) {
        additionalInformation = person.occupation;
    }
    console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}

export function filterUsers(persons: Person[], criteria: Partial<User>): User[] {
    return persons.filter(isUser).filter((user) => {
        const criteriaKeys = Object.keys(criteria) as (keyof User)[];
        return criteriaKeys.every((fieldName) => {
            return user[fieldName] === criteria[fieldName];
        });
    });
}

console.log('Users of age 23:');

filterUsers(
    persons,
    {
        age: 23
    }
).forEach(logPerson);

두 번째 풀이 - omit 이용

omit을 통해 type을 제거한다. 아래의 criteriaKeys에서도 타입을 조정해 user[fieldName] === criteria[fieldName] 에서도 오류가 나지 않도록 한다.

export function filterUsers(persons: Person[], criteria: Partial<Omit<User, 'type'>>): User[] {
    return persons.filter(isUser).filter((user) => {
        const criteriaKeys = Object.keys(criteria) as (keyof Omit<User, 'type'>)[];
        return criteriaKeys.every((fieldName) => {
            return user[fieldName] === criteria[fieldName];
        });
    });
}

관련 개념

Utility types - Partial
타입의 모든 속성들을 optional로 만든다.

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo2 = updateTodo(todo1, {
  description: "throw out trash",
});

Utility types - Omit
Omit<Type, Keys> 에서 keys에 해당하는 값들을 제거한다.

interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
  createdAt: 1615544252770,
};

6번

export function filterPersons(persons: Person[], personType: 'user', criteria: Partial<User>): User[];
export function filterPersons(persons: Person[], personType: 'admin', criteria: Partial<Admin>): Admin[];
export function filterPersons(persons: Person[], personType: string, criteria: Partial<Person>): Person[] {
    return persons
        .filter((person) => person.type === personType)
        .filter((person) => {
            let criteriaKeys = Object.keys(criteria) as (keyof Person)[];
            return criteriaKeys.every((fieldName) => {
                return person[fieldName] === criteria[fieldName];
            });
        });
}

Bonus 풀이

export function getObjectKeys(criteria: Partial<Person>) {
    return Object.keys(criteria) as (keyof Person)[];
}

export function filterPersons(persons: Person[], personType: 'user', criteria: Partial<User>): User[];
export function filterPersons(persons: Person[], personType: 'admin', criteria: Partial<Admin>): Admin[];
export function filterPersons(persons: Person[], personType: string, criteria: Partial<Person>): Person[] {
    return persons
        .filter((person) => person.type === personType)
        .filter((person) => {
            let criteriaKeys = getObjectKeys(criteria);
            return criteriaKeys.every((fieldName) => {
                return person[fieldName] === criteria[fieldName];
            });
        });
}

getObjectKeys 함수를 통해 type cast를 filterPersons 함수에서 제거할 수 있다.
하지만 이는 문제에서 요구한 argument의 타입에 따라 result를 준다는 요구사항에 맞지 않는다. 따라서 제네릭을 사용해 아래와 같이 변경하면 된다.

Bonus 풀이 - Generic

const getObjectKeys = <T>(arg: T) => Object.keys(arg) as (keyof T)[];

export function filterPersons(persons: Person[], personType: 'user', criteria: Partial<User>): User[];
export function filterPersons(persons: Person[], personType: 'admin', criteria: Partial<Admin>): Admin[];
export function filterPersons(persons: Person[], personType: string, criteria: Partial<Person>): Person[] {
    return persons
        .filter((person) => person.type === personType)
        .filter((person) => {
            let criteriaKeys = getObjectKeys(criteria);
            return criteriaKeys.every((fieldName) => {
                return person[fieldName] === criteria[fieldName];
            });
        });
}

위와 같이 변경해 argument의 타입에 따라 객체의 값들을 type cast해서 결과를 리턴해주는 함수를 통해 filterPersons 함수를 리팩토링 할 수 있다.

관련 개념

Function overload
argument의 종류 / 개수가 여러개일 때, 오버로딩을 사용할 수 있다.

export function filterPersons(persons: Person[], personType: 'user', criteria: Partial<User>): User[];
export function filterPersons(persons: Person[], personType: 'admin', criteria: Partial<Admin>): Admin[];
export function filterPersons(persons: Person[], personType: string, criteria: Partial<Person>): Person[] {
  
  ...
  
}

위와 같이 함수를 선언한 경우,

export const usersOfAge23 = filterPersons(persons, 'user', { age: 23 });
export const adminsOfAge23 = filterPersons(persons, 'admin', { age: 23 });

usersOfAge23에서는 위의 User[] 타입이 리턴되는 함수가,
adminsOfAge23에서는 위의 Admin[] 타입이 리턴되는 함수가 호출된다.

Generics
아래 함수에서는 argument 타입에 따라 리턴 값의 타입이 결정된다.

function identity<Type>(arg: Type): Type {
  return arg;
}

0개의 댓글