ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TS] JavaScript에 없는 튜플 (tuple) 타입
    Front-end 개발 2024. 2. 3. 15:47

    TypeScript

    타입스크립트(TypeScript, TS) 튜플(tuple) 타입에 대해 다뤄본다.

     

    JavaScript의 타입(Type)


    JavaScript는 동적 타입 언어(Dynamic Type Language)이기 때문에 정적 언어와 달리 자료형을 정의할 필요가 없다. 즉, 컴파일 과정이 아닌 런타임(코드 실행) 도중에 JavaScript Engine이 자료형을 확인한다. 변수의 경우 변숫값이 할당될 때 해당 값의 타입에 따라 변수 타입이 결정된다.

     

    이런 JavaScript에서는 총 8개의 타입(type) 또는 자료형이 있다. 7개는 불변한 원시 값(Primitive values)으로 문자열(String), 숫자형(Number), 논리형(Boolean), Undefined, Null, Symbol, BigInt 까지 총 7개의 (원시)자료형을 포함하고, 나머지 하나는 객체(Object)로 JavaScript에서 유일한 변경 가능한 값이다. (출처: mdn, JavaScript 타입과 자료구조)

     

    그렇다면 배열(Array), 함수(Function), 클래스(Class)는 자료형이 뭘까?

    객체는 키/값 쌍(key-value pair)에 데이터를 저장한다. 이러한 데이터를 컬렉션(collection)이라 하며, 개발을 하다 보면 정렬을 하기 위해 순서가 있는 컬렉션이 필요할 때가 생긴다. 이때 쓰이는 자료구조가 바로 배열이다. 객체는 태생이 순서를 고려하지 않고 만들어진 자료구조이기 때문이다. (출처: 모던 JavaScript) 배열 정의는 일반적으로 "리스트 같은 객체(list-like objects)", 리스트에 저장된 다수의 값들을 포함하고 있는 하나의 객체라고 기술한다. (출처: mdn, 배열) 다시 말해, 항목으로 이뤄진 목록만 저장할 때는 컬렉션으로 이루어진 객체를 만들 필요가 없으며, JavaScript 배열은 순서대로 값을 저장하는 객체 타입이라고 풀어 쓸 수 있다.

     

    추가적으로 함수와 클래스 또한 객체 타입임을 알 수 있다. 함수는 일급 객체에 해당하며, 클래스는 JavaScript에서 함수의 일종이기 때문이다.

     

    함수
    JavaScript에서 함수는 다른 함수로 전달되거나 반환받을 수 있고, 변수와 속성을 할당받을 수도 있기 때문에 일급 객체에 해당한다. 또한 다른 객체와 마찬가지로 속성과 메서드를 가질 수 있습니다. 다른 객체와 구별되는 점은 함수를 호출할 수 있다는 점입니다. (출처: mdn, 함수)

     

    Class 정의
    Class는 사실 "특별한 함수"입니다. 함수를 함수 표현식과 함수 선언으로 정의할 수 있듯이 class 문법도 class 표현식과 class 선언 두가지 방법을 제공합니다. (출처: mdn, Classes)

     

    TypeScript의 타입


    TypeScript에서 다루는 타입은 String, Number, Boolean, Array, Tuple, Enum(열거형), Any, Unkown, Object, Null/Undefined, void, function, Never, Union, Intersection 이 있다.

    이중에서 이번 글에서 다루는 주제는 Array와 Tuple 타입이다.

    TS 배열 타입 (Array)


    TypeScript에서 배열인 경우 아래와 같이 두 가지 방식으로 선언할 수 있다.

    주로 아래 코드 예시와 같이 Type[] 형태의 구문을 사용한다. 숫자형 배열을 선언할 경우 소문자로 시작하는 number 타입을 먼저 써주고 대괄호([])를 뒤에 써준다. 이 구문은 모든 타입에서 사용할 수 있으며, 동일한 의미로 Array<Type> 와 같은 형태를 사용할 수도 있다. 대문자로 시작하는 두번째 방식은 제네릭(generic)으로 뒤에서 자세히 배울 것이므로 언급만 하고 넘어가도록 한다.

    주의할 점은 number[]와 [number]와 전혀 다른 의미를 가진다. [number]는 튜플(tuple)을 정의하는 형태의 구문으로 다음 주제인 튜플(tuple) 타입에서 다룬다.

    let arr: number[] = [1,2,3];
    let arr1: Array<number> = [1,2,3]; // generic

     

    원시형 데이터로 구성된 배열을 각각 선언해보면 아래 코드와 같다.

    대표적인 원시형 데이터 타입인 number, string, boolean은 일반적이다. null과 undefined는 null이나 undefined 외 다른 타입의 데이터가 할당되면 에러가 발생하기 때문에 코드 상에서 그다지 유용하지 않은 배열이다. 또한 any와 unknown 타입 배열의 경우에 선언을 해도 TypeScript Compiler(tsc) 에러가 발생하지는 않지만, 엄격한 타입 언어인 TypeScript를 사용하는 의미가 없어지므로 사용하지 않는 것이 좋다.

    여기서는 단일 속성을 주로 다루지만, 이후에 배울 유니온(union) 타입을 이용하면 any로 선언한 배열과 같이 다양한 타입을 가진 배열을 정의할 수 있다.

    const arrNum: number[] = [1,2,3];
    const arrString: string[] = ['a','b','c'];
    const arrBoolean: boolean[] = [true, false, true];
    
    // Not usefull
    const arrNull: null[] = [null, null, null];
    const arrUndefined: undefined[] = [undefined, undefined, undefined];
    
    // Don't use it
    const arrAny: any[] = [1, 'a', true];
    const arrUnkown: unknown[] = [1, 'a', true];

     

    배열 선언 시에 readonly 접근 제한자를 추가하여 읽기 전용 배열로 정의할 수 있다. readonly 접근 제한자는 배열의 내용을 변경할 수 없으며, 변경할 염려 없이 사용하겠다는 의도를 담을 수 있다. Array<Type>을 Type[]으로 단축해서 사용한 것과 같이 ReadonlyArray<Type>를 동일한 의미와 기능으로 readonly Type[]으로 단축 구문으로 쓸 수 있다.

    const arrReadonly: readonly string[] = ["a", "b", "c"];
    const arrReadonly1: ReadonlyArray<string> = ["a", "b", "c"];

     

    readonly 또는 ReadonlyArray는 변경해서는 안되는 배열을 설명하는 특수 타입이다. 주의할 점은 읽기 전용 접근 제한자(readonly modifier)가 있는 배열은 일반 배열의 할당이 가능하지만, 일반 배열은 읽기 전용 배열을 할당할 수 없다.

    let arrReadonly3: readonly string[] = ['a','b'];
    let arrNormal: string[] = ['c'];
    
    arrReadonly.push("hello"); // error! 'readonly string[]'
    
    arrReadonly3 = arrNormal; // ok
    console.log(arrReadonly); // ["c"]
    
    arrNormal = arrReadonly3; // error! 'readonly'

     

    TS 다차원 배열


    대괄호([])를 자료형 다음에 연달아 작성해 다차원 배열 타입도 정의할 수 있다. 차원이 올라갈 수록 대괄호가 하나씩 덧붙여 지는데, 4차원 배열에는 3차원에 (2, 3) 배열을 추가해주므로써 복잡하게 정의 해봤다. 각 차원의 길이는 0번 인덱스의 length 속성으로 접근할 수 있다.

    const arr2D: number[][] = [
        [1, 2, 3],
        [4, 5, 6]
    ];
    
    const arr3D: number[][][] = [[
        [1, 2, 3],
        [4, 5, 6]
    ]];
    
    const arr4D: number[][][][] = [[
        [
            [1, 2, 3],
            [4, 5, 6]
        ],
        [
            [7, 8, 9],
            [10, 11, 12]
        ]
    ]];
    
    console.log(arr2D); // [[1,2,3],[4,5,6]], (2, 3)
    console.log(arr3D); // [[[1,2,3],[4,5,6]]], (1, 2, 3)
    console.log(arr4D); // [[[[1,2,3],[4,5,6]], [[7,8,9],[10,11,12]]]], (1, 2, 2, 3)
    
    console.log(arr4D.length); // 1, [Array(2)]
    console.log(arr4D[0].length); // 2, [Array(2), Array(2)]
    console.log(arr4D[0][0].length); // 2, [Array(3), Array(3)]
    console.log(arr4D[0][0][0].length); // 3, [1, 2, 3]

     

    TS 튜플 타입 (Tuple)


    튜플 타입은 배열 타입의 또 다른 종류로써, 포함된 요소(elements 또는 items)의 수와 특정 위치에 포함된 타입을 정확히 알 수 있다. 변수를 정의할 때 튜플의 타입 작성 방법은 [Type1, Type2, Type3]와 같이 대괄호([]) 안에 타입의 순서에 맞게 써준다. 문자열과 논리형을 아이템으로 가지는 튜플 타입은 아래와 코드와 같이 작성해줄 수 있다.

    let tuple: [string, boolean] = ['a', true];

     

    타입 시스템에서 앞서 정의했던 tuple 변수는 0 번 인덱스(index)에 문자열이 들어있고, 1번 인덱스에 숫자가 들어 있는 배열을 의미한다. 튜플은 일반 배열 타입과 달리 포함된 아이템의 수와 위치가 중요한 타입이므로, 타입과 다른 값을 할당하거나 index를 초과하여 조회 및 수정하면 에러가 발생한다.

    tuple = [true, 'a']; // error!
    tuple[2] = 'two'; // error! no element

     

    튜플 타입은 JavaScript에서와 같이 배열 구조 분해 할당(destructuring)을 사용할 수 있다.

    const stringNumberPair: [string, number] = ['a', 1];
    
    const [inputString, inputNumber] = stringNumberPair; // Array destructuring
    
    console.log(inputString); // 'a'
    console.log(inputNumber); // 1

     

    정해진 타입의 순서와 고정된 길이의 배열을 표현하는 튜플은 할당(Assign)에 국한된다. 배열의 .push(), .splice() 등 메소드를 통해 값을 수정하는 행위는 막을 수 없다. 물론 튜플 정의에 없는 다른 타입을 추가할 수는 없다.

    console.log(stringNumberPair); // ["a", 1]
    
    //stringNumberPair.push(true); // error! type 'string | number'
    
    stringNumberPair.push(100);
    console.log(stringNumberPair) // ["a", 1, 100]
    
    stringNumberPair.splice(1);
    console.log(stringNumberPair) // ["a"]
    
    stringNumberPair.pop();
    console.log(stringNumberPair) // []

     

    튜플 타입은 물음표(?) 기호를 사용하여 선택적 속성을 가질 수 있다. 주의할 점은 선택적 튜플 요소는 가장 마지막 요소에만 올 수 있으며 길이 속성에도 영향을 준다. 물음표가 붙은 마지막 배열의 요소는 특정 타입 또는 undefined 타입을 가진다. 즉, 변수 선언 시에 마지막 요소에 대응되는 값이 할당되지 않아도 tsc 에러가 발생하지 않으며, undefined가 할당된다.

    const Either2Or3: [number, number, number?] = [1, 2];
    
    // const Either2Or3: [number, number, (number | undefined)?]
    console.log(Either2Or3[2]); // undefined
    
    // (property) length: 2 | 3
    console.log(Either2Or3.length); // 2

     

    튜플에는 배열/튜플 유형과 함께 spread 또는 rest 연산자(...)를 사용할 수 있다. 튜플의 이런 기능은 최소 원소 수를 지정할수 있음과 동시에 다양한 수의 인수를 받을 수 있고, 불필요한 반복없이 코드를 간결하게 작성할 수 있다.

    const stringNumberBooleans: [string, number, ...boolean[]] = ['a', 1, true, false];
    const stringBooleansNumber: [string, ...boolean[], number] = ['a', true, false, 1];
    const BooleansStringNumber: [...boolean[], string, number] = [true, false, 'a', 1];

     

    아직 배우진 않았지만, 튜플의 구조 분해 할당과 rest 연산자의 조합은 몇 개가 들어올지 모르는 함수의 매개변수에 유용하게 사용할 수 있다.

    function myFnc (...args: [string, number, ...boolean[]) {
        // name: string, version: number, ...input: boolean[]
    	const [name, version, ...input] = args;
        // ...
    }

     

    배열 타입과 마찬가지로 튜플 타입에도 readonly 접근 제한자를 사용할 수 있다.

    const tupleReadonly: readonly [string, number] = ['a', 1];
    tupleReadonly[0] = 'two'; // error! readonly property

     

    튜플은 대부분의 코드에서 생성된 후 변경하지 않는 경향이 있다. 그렇기 때문에 가능하면 읽기 전용 튜플로 생성하여 객체를 어떻게 사용해야 하는지에 대한 의도를 개발 단계에서 담아주는 것이 좋다. (출처: TypeScript 핸드북)

     

    TS 다차원 튜플


    다차원 튜플은 대괄홀([])를 덧붙여 나가던 방식과 조금 달리 차원일 올라갈 수록 대괄호([])를 덧씌워주며 작성하면 된다. 아래 예제에서는 튜플 타입을 요소로 가지는 2차원 배열 타입과, rest 연산자를 사용한 배열을 이용해 튜플 타입을 요소로 가지는 2, 3, 4차원 튜플을 생성해 보았다. [...number[]]는 [number, number, number]와 동일한 의미이다.

    // 2D Array
    const tupleInArr: [number, string, boolean][] = [[1, 'a', true], [2, 'b', false], [3, 'c', true]];
    
    const tuple2D: [[...number[]], [...string[]]] = [
        [1, 2, 3],
        ['a', 'b', 'c']
    ];
    
    const tuple3D: [[[...number[]], [...string[]]]] = [[
        [1, 2, 3],
        ['d', 'e', 'f']
    ]];
    
    const tuple4D: [[[[...number[]], [...string[]]],[[...number[]], [...string[]]]]] = [[
        [
            [1, 2, 3],
            ['a', 'b', 'c']
        ],
        [
            [7, 8, 9],
            ['d', 'e', 'f']
        ]
    ]];
    
    console.log(tuple2D); // [[1, 2, 3], ["a", "b", "c"]] 
    console.log(tuple3D); // [[[1, 2, 3], ["a", "b", "c"]]] 
    console.log(tuple4D); // [[[[1, 2, 3], ["a", "b", "c"]], [[7, 8, 9], ["d", "e", "f"]]]]
    console.log(tuple4D.length); // 1, [Array(2)]
    console.log(tuple4D[0].length); // 2, [Array(2), Array(2)]
    console.log(tuple4D[0][0].length); // 2, [Array(3), Array(3)]
    console.log(tuple4D[0][0][0].length); // 3, [1, 2, 3]

     

     

    참고자료1: 공식 Docs - 타입스크립트 핸드북

    참고자료2: 한눈에 보는 타입스크립트

    참고자료3: 캡틴판교 타입스크립트 핸드북

    참고자료4: 한 입 크기로 잘라먹는 타입스크립트

Designed by Tistory.