ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • typescript - exercises 2일차
    Typescript 2023. 7. 11. 16:38

    🔔 서론

    1일차가 지나고 다음 문제를 풀려는데 5번부터 막혀 부족함을 깨달아서 상당히 스스로에게 화가 난 상태로 문제를 풀어냈다... (화가 난 상태면 내가 뭘 할 수 있는데ㅋㅋ)

    https://typescript-exercises.github.io/

     

    TypeScript Exercises

    A set of interactive TypeScript exercises

    typescript-exercises.github.io

     

    이번 포스트는 위 사이트의 5~8번 문제를 풀며 각 문제 풀이에 해당하는 핵심 키워드 개념을 정리하는 글이다.


    🥁 5번

    Utility type을 적절히 사용하여 타입의 변환을 주는 문제이다.

    문제의 요구사항에서 '필요한 기준'만 통과 되게끔 filterUsers를 변경해야한다 라고 서술했고, filterUsers를 호출하는 라인에서 파라미터로 { age: 23 }을 주었다.

    그렇다면 age 속성 외에 다른 속성들은 모두 optianal이 되어야 해당 조건을 만족할 것이다. (age 역시 포함)

    이러한 역할을 해주는 Utility type이 Partial이다.

    🌈 Partial<Type>

    Partial<Type>은 모든 속성이 optional로 설정된 Type을 생성한다. 즉, 주어진 Type의 모든 하위 집합을 나타내는 Type을 반환하는 것이다.

    // ex)
    interface Person {
        name: string,
        age: number,
        height: number
    }
    
    function returnPerson(person: Partial<Person>) {
        return person
    }
    
    const jack = returnPerson({age: 23})	// only age property
    const peter = returnPerson({name: 'peter'})	// only name property
    
    console.log(jack, peter)	// {age: 23}, {name: 'peter'}

    첫 번째 요구사항은 해결되었고, 추가 요구사항에서는 'type'이라는 속성을 criteria에서 제외시키라고 한다.

    특정 속성을 제외한 type을 반환하는 것은 Utility type에서 Omit을 사용하면 된다.

    🌈 Omit<Type, keys>

    Omit<Type, keys>은 Type의 모든 속성중 keys에 해당되는 속성을 제외한 Type을 생성한다.

    // ex)
    interface Person {
        name: string,
        age: number,
        height: number
    }
    
    type PersonExceptHeight = Omit<Person, 'height'>
    	
    const jack: Person = {name: 'jack', age: 23, height: 180}	// ok
    const peter: PersonExceptHeight = {name: 'peter', age: 23, height: 170} // error

     

    따라서, 모든 속성이 optional하고, 'type'속성을 제외한 타입은 Partial<Omit<Type, keys>> 으로 지정해 주면 된다.

    /*
    Exercise:
        Without duplicating type structures, modify
        filterUsers function definition so that we can
        pass only those criteria which are needed,
        and not the whole User information as it is
        required now according to typing.
    
    Higher difficulty bonus exercise:
        Exclude "type" from filter criteria.
    */
    
    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<Omit<User, 'type'>>): User[] {
        return persons.filter(isUser).filter((user) => {
            const criteriaKeys = Object.keys(criteria) as (keyof Partial<Omit<User, 'type'>>)[];
            return criteriaKeys.every((fieldName) => {
                return user[fieldName] === criteria[fieldName];
            });
        });
    }
    
    console.log('Users of age 23:');
    
    filterUsers(
        persons,
        {
            age: 23
        }
    ).forEach(logPerson);

    🥁 6번

    Overloading과 Generic type을 활용해 해결하는 문제이다.

    🌈 Overloading

    overloading은 함수가 서로 다른 여러개의 call signature를 가지고 있을 때 발생하는 것으로 대체로 매개 변수의 개수는 동일하지만, 타입이 다른 경우와 매개변수의 개수가 다른 경우가 있다.

    // ex1) 매개변수 개수는 같은데 타입이 다른 경우
    type Add = {
        (a: number, b: number): number
        (a: number, b: string): number
    }
    
    const add: Add = (a, b) => {
        return (typeof b === "string") ? a : a + b // b가 string type이면 a 반환 아닐 시 a + b 반환
    }
    
    console.log(add(1, 2)) // 3
    console.log(add(1, "2")) // 1
    // ex1) 매개변수 개수가 다른 경우
    type Add = {
        (a: number, b: number): number
        (a: number, b: number, c: number): number
    }
    
    const add: Add = (a, b, c?: number) => {
        return c ? a + b + c : a + b // c가 존재할 시 a + b + c 반환 아닐 시 a + b 반환
    }
    
    console.log(add(1, 2)) // 3
    console.log(add(1, 2, 3)) // 6

     

    🌈 Generic type

    Generic type은 어떤 type이 들어올 지 확실하게 알 수 없을 때 사용하는 일종의 type placeholder 역할을 한다.

    추가 요구사항으로 criteria의 keys를 뽑는 코드 Object.keys(criteria) as (key of User)[] 를 따로 빼서 getObjectKeys 함수로 호출 하는 것이 주어졌다. filterPersons 함수 바깥에서는 getObjectKeys에 어떤 타입이 들어갈지 모르는 상태이기 때문에 Generic type을 통해 해결해야한다.

    // ex)
    const arrayLength = <T>(array: T[]): number => array.length
    
    let numArray: number[] = [1, 2, 3]
    let stringArray: string[] = ['hello', 'world']
    
    console.log(
        arrayLength(numArray),
        arrayLength(stringArray)
    )	// 3, 2

     

    🤔 그럼 Generic type이나 any 타입이나 같은거 아닐까?

    generic을 정확히 모르고 사용하다가는 any과 크게 다를바 없다고 혼동할 수도 있다. 예시를 통해 둘의 차이를 살펴보자.아래의 코드는 각각 any type으로 만들어진 배열과 generic type으로 만들어진 배열의 첫 번째 요소에 toUpperCase로 대문자로 변환시키려는 코드이다. any type인 array는 a.toUpperCase 구문에서 에러를 발생시키지 않고, 컴파일을 진행해야 에러를 발생시키는데 any type은 typescript의 보호를 받고 있지 않기 때문이다. 반면, generic type인 array는 b.toUpperCase 구문에서 에러를 발생시킨다. 해당 구문을 보호하기 위해서는 b의 타입이 string인지 확인하는 방어코드를 작성해주어야 에러가 사라질 것이다.

    // any
    type SuperPrintAny = {
        (arr: any[]): any
    }
    
    const superPrintAny: SuperPrintAny = (arr) => arr[0]
    
    let a = superPrintAny([1, 'b', true])
    
    a.toUpperCase() // no error
    
    // generic
    type SuperPrintGeneric = {
        <T>(arr: T[]): T
    }
    
    const superPrintGeneric: SuperPrintGeneric = (arr) => arr[0]
    
    let b = superPrintGeneric([1, 'b', true])
    
    b.toUpperCase() // error

     

    따라서, getObjectKeys에는 어떤 타입이 들어올지 모르기 때문에 함수의 내용을 <T>(obj: T) => Object.keys(obj) as (keyof T)[] 로 바꿔주면 된다.

    /*
    Exercise:
        Fix typing for the filterPersons so that it can filter users
        and return User[] when personType='user' and return Admin[]
        when personType='admin'. Also filterPersons should accept
        partial User/Admin type according to the personType.
        `criteria` argument should behave according to the
        `personType` argument value. `type` field is not allowed in
        the `criteria` field.
    
    Higher difficulty bonus exercise:
        Implement a function `getObjectKeys()` which returns more
        convenient result for any argument given, so that you don't
        need to cast it.
    
        let criteriaKeys = Object.keys(criteria) as (keyof User)[];
        -->
        let criteriaKeys = getObjectKeys(criteria);
    */
    
    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: 'Anti-virus engineer' }
    ];
    
    export function logPerson(person: Person) {
        console.log(
            ` - ${person.name}, ${person.age}, ${person.type === 'admin' ? person.role : person.occupation}`
        );
    }
    
    const getObjectKeys = <T>(obj: T) => Object.keys(obj) as (keyof T)[];
    
    export function filterPersons(persons: Person[], personType: User['type'], criteria: Partial<Omit<User, 'type'>>): User[];
    export function filterPersons(persons: Person[], personType: Admin['type'], criteria: Partial<Omit<Admin, 'type'>>): Admin[];
    export function filterPersons(persons: Person[], personType: Person['type'], criteria: Partial<Omit<Person, 'type'>>): Person[] {
        return persons
            .filter((person) => person.type === personType)
            .filter((person) => {
                let criteriaKeys = getObjectKeys(criteria);
                return criteriaKeys.every((fieldName) => {
                    return person[fieldName] === criteria[fieldName];
                });
            });
    }
    
    export const usersOfAge23 = filterPersons(persons, 'user', { age: 23 });
    export const adminsOfAge23 = filterPersons(persons, 'admin', { age: 23 });
    
    console.log('Users of age 23:');
    usersOfAge23.forEach(logPerson);
    
    console.log();
    
    console.log('Admins of age 23:');
    adminsOfAge23.forEach(logPerson);

    🥁 7번

    Generic type과 tuple type을 활용해 해결하는 문제이다.

    🌈 Tuple type

    Tuple type은 인덱스의 타입이 정해져 있고, 요소의 수가 고정된 배열을 표현하는 경우 사용되는 type이다.

    type tuppleArray = [string, number, boolean]
    
    const a: tuppleArray = ['hello', 1, true]
    const b: tuppleArray = [1, true, 'hello']  // error

     

    따라서, swap함수는 파라미터의 타입을 알 수 없기 때문에 generic 타입 <T, V>로 타입을 정해주고 리턴 타입으로 tuple type [V, T]를 리턴해주면 된다.

    /*
    Exercise:
        Implement swap which receives 2 persons and returns them in
        the reverse order. The function itself is already
        there, actually. We just need to provide it with proper types.
        Also this function shouldn't necessarily be limited to just
        Person types, lets type it so that it works with any two types
        specified.
    */
    
    interface User {
        type: 'user';
        name: string;
        age: number;
        occupation: string;
    }
    
    interface Admin {
        type: 'admin';
        name: string;
        age: number;
        role: string;
    }
    
    function logUser(user: User) {
        const pos = users.indexOf(user) + 1;
        console.log(` - #${pos} User: ${user.name}, ${user.age}, ${user.occupation}`);
    }
    
    function logAdmin(admin: Admin) {
        const pos = admins.indexOf(admin) + 1;
        console.log(` - #${pos} Admin: ${admin.name}, ${admin.age}, ${admin.role}`);
    }
    
    const admins: Admin[] = [
        {
            type: 'admin',
            name: 'Will Bruces',
            age: 30,
            role: 'Overseer'
        },
        {
            type: 'admin',
            name: 'Steve',
            age: 40,
            role: 'Steve'
        }
    ];
    
    const users: User[] = [
        {
            type: 'user',
            name: 'Moses',
            age: 70,
            occupation: 'Desert guide'
        },
        {
            type: 'user',
            name: 'Superman',
            age: 28,
            occupation: 'Ordinary person'
        }
    ];
    
    export function swap<T, V>(v1: T, v2: V): [V, T] {
        return [v2, v1];
    }
    
    function test1() {
        console.log('test1:');
        const [secondUser, firstAdmin] = swap(admins[0], users[1]);
        logUser(secondUser);
        logAdmin(firstAdmin);
    }
    
    function test2() {
        console.log('test2:');
        const [secondAdmin, firstUser] = swap(users[0], admins[1]);
        logAdmin(secondAdmin);
        logUser(firstUser);
    }
    
    function test3() {
        console.log('test3:');
        const [secondUser, firstUser] = swap(users[0], users[1]);
        logUser(secondUser);
        logUser(firstUser);
    }
    
    function test4() {
        console.log('test4:');
        const [firstAdmin, secondAdmin] = swap(admins[1], admins[0]);
        logAdmin(firstAdmin);
        logAdmin(secondAdmin);
    }
    
    function test5() {
        console.log('test5:');
        const [stringValue, numericValue] = swap(123, 'Hello World');
        console.log(` - String: ${stringValue}`);
        console.log(` - Numeric: ${numericValue}`);
    }
    
    [test1, test2, test3, test4, test5].forEach((test) => test());

    🥁 8번

    Union type의 intersection type과 Utility type을 활용해 해결하는 문제이다.

    🌈 Intersection type

    intersection type은 Union type의 종류로 여러 타입을 모두 만족하는 하나의 타입을 의미한다.

    따라서, PowerUser 의 type 속성값은 'poserUser'이므로 User와 Admin에서 각각 'type'속성을 제외 한 타입과 {type: 'powerUser'} 을 모두 합치면 된다.

    /*
    Exercise:
        Define type PowerUser which should have all fields
        from both User and Admin (except for type),
        and also have type 'powerUser' without duplicating
        all the fields in the code.
    */
    
    interface User {
        type: 'user';
        name: string;
        age: number;
        occupation: string;
    }
    
    interface Admin {
        type: 'admin';
        name: string;
        age: number;
        role: string;
    }
    
    type PowerUser = Omit<User, 'type'> & Omit<Admin, 'type'> & {type: 'powerUser'};
    
    export type Person = User | Admin | PowerUser;
    
    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: 'powerUser',
            name: 'Nikki Stone',
            age: 45,
            role: 'Moderator',
            occupation: 'Cat groomer'
        }
    ];
    
    function isAdmin(person: Person): person is Admin {
        return person.type === 'admin';
    }
    
    function isUser(person: Person): person is User {
        return person.type === 'user';
    }
    
    function isPowerUser(person: Person): person is PowerUser {
        return person.type === 'powerUser';
    }
    
    export function logPerson(person: Person) {
        let additionalInformation: string = '';
        if (isAdmin(person)) {
            additionalInformation = person.role;
        }
        if (isUser(person)) {
            additionalInformation = person.occupation;
        }
        if (isPowerUser(person)) {
            additionalInformation = `${person.role}, ${person.occupation}`;
        }
        console.log(`${person.name}, ${person.age}, ${additionalInformation}`);
    }
    
    console.log('Admins:');
    persons.filter(isAdmin).forEach(logPerson);
    
    console.log();
    
    console.log('Users:');
    persons.filter(isUser).forEach(logPerson);
    
    console.log();
    
    console.log('Power users:');
    persons.filter(isPowerUser).forEach(logPerson);

    💊 2일차 후기

    4번문제를 풀 때 까지는 쉬운 난이도에 가볍게 생각하고 있었다가 5번부터 개념을 모르면 풀 수가 없는 문제였기에 심히 당황스러웠다. 특히 generic type과 overloading은 보면 볼 수록 헷갈리는 내용이기에 복습을 다시 해야겠다는 생각을 가지게 되었다.

Designed by Tistory.