티스토리 뷰
글을 읽으시면서 모르는 개념이 나오더라도 바로바로 따라 해 볼 수 있게 작성하였습니다.
TS가 처음이시라면 아래 글들이 이해에 도움이 될 것 같습니다.⭐
- [TS] TypeScript 시작하기
- [TS] TypeScript의 기본 타입
- [TS] TypeScript의 타입 조작 (제너릭, 조건부타입, infer)
- [TS] 유틸리티(Utility) 타입
JS의 String.prototype.split()을 모방하여 타입추론이 가능한 문자열 분할 타입, Split를 새롭게 구현합니다.
String.prototype.split()
/**
* Split a string into substrings using the specified separator and return them as an array.
*@paramseparator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned.
*@paramlimit A value used to limit the number of elements returned in the array.
*/
split(separator: string | RegExp, limit?: number): string[];
자바스크립트에는 String.prototype.split()이라는 문자열 메소드가 있습니다.
인수로 `separator`라는 문자열을 받아 이를 기준으로 대상 문자열을 잘라 배열로 반환하는 메서드인데요.
'abcdef'.split('c'); ["ab", "def"]
예를 들어 abcdef.split('c')는 ['ab', 'def'] 라는 결과로 반환됩니다.
문자열 `'abcdef'`를 `'c'`를 기준으로 잘라`'ab', 'def'`을 만들고, 이를 배열에 담아 반환합니다.
'abcdef'.split('c'); // type is string[].
보시다시피 타입은 그냥 string 배열로 추론이 됩니다.
우리는 'abcdef' 라는 문자열을 'c'로 자른다는 것을 확인할 수 있는데 기존 메서드에서는 추론되지 않는 거죠.
타입추론만으로 결과를 바로 알 수 없을까요?
이 메서드를 커스텀 타입으로 다시 만들어 봅시다!✌️
namespace 선언
먼저 StringType이라는 namespace(네임스페이스)를 만들고 그 안에 Split 타입을 선언해 보겠습니다.
네임스페이스는 코드를 조직화하고 모듈화 하는 데 사용되는 일종의 구조화된 컨테이너입니다.
주로 객체로 사용되며, 내부에 함수, 변수, 클래스 등을 포함할 수 있습니다.
네임스페이스를 사용하면 전역 스코프에서 이름 충돌을 방지하고 관련 있는 코드를 그룹화하여 유지보수가 편리해집니다!
자바스크립의 모듈 시스템과 유사한 기능이죠.
export namespace StringType {
export type Split = any
}
type test = StringType.Split // type test = any
이렇게 네임스페이스를 통해 선언된 타입은 메서드처럼 점 연산자(.)를 이용해 가져올 수 있게 됩니다.
클래스와의 차이?
클래스는 객체를 생성하여 사용하는 반면, 네임스페이스는 객체를 생성하지 않고 그룹화된 코드를 접근하는 데 사용됩니다.
타입 파라미터 명시하기
현재 Split 타입은 any를 대입해 두었기 때문에 무조건 any로 추론될 뿐, 아무런 역할을 하지 못합니다.
이제 타입을 함수의 파라미터처럼 다루기 위해, 제네릭으로 된 타입 파라미터를 추가해 보겠습니다.
export namespace StringType {
export type Split<T extends string> = any;
}
먼저 자를 대상이 되는 문자열에 대한 타입 파라미터의 T를 추가했습니다.
extends는 타입제한자 키워드로 타입 파라미터의 범위를 제한하는 데에 사용됩니다.
`T extends string`은 `T는 최소한 string타입`이라는 의미이죠.
타입 제한자(Type Constraint)와 조건부 타입(Conditional Type) ⭐
- 타입제한자 : extends 키워드를 사용하여 제한하려는 타입을 지정합니다.
- 조건부타입 : extends 키워드와 삼항 연산자(? :)를 사용합니다. 특정 조건에 따라 타입을 변환하거나 결정할 때 사용합니다.
타입 제한자는 주로 제네릭 함수나 클래스에서 사용되며, 특정 타입이나 속성을 갖도록 강제하는 데 사용됩니다.
조건부 타입은 주로 타입 변환에 사용되며, 입력된 타입에 따라 다른 동작을 구현할 때 유용합니다.
각각의 기능을 올바르게 이해하고 사용해야 타입 시스템을 더욱 효과적으로 활용할 수 있습니다!
export namespace StringType {
export type Split<T extends string, S extends string> = any;
}
이어서 구분자에 대한 타입파라미터 S도 추가해 보았습니다.
구분자를 주지 않으면 전체 문자열을 단일 문자로 나누어 배열로 반환하도록 옵션을 넣어볼까요.
단일 문자로 나눈다는 건, 공백 `''`으로 문자열을 나눈 것과 같습니다.
export namespace StringType {
export type Split<T extends string, S extends string = ''> = any;
}
이를 반영하기 위해 S의 기본 타입을 문자열 리터럴 타입 `''`으로 명시해 주었습니다.
함수에 기본 값을 명시하는 것처럼 타입에도 기본 타입을 명시할 수 있는데요.
타입 뒤에 `=` 기호를 붙이고 지정할 기본 타입을 작성하면 됩니다.
문자열 리터럴 타입이라는 것은 이 타입이 단순히 string이 아니라, string보다도 더 협소한 범위의 문자열을 의미합니다.
'helloworld'처럼 `값이 확정된 문자열`들을 말하죠.
즉, S 타입 파라미터에 아무런 값이 들어오지 않는다면 `''`이 구분자가 됩니다.
StringType.Split 타입의 구현 부분 작성하기
커스텀 타입을 만드는 일은 함수를 만드는 것과 동일합니다.
함수를 정의하는 것과 마찬가지로 먼저 파라미터에 대한 타입을 명시하고, 구현 부분을 작성하는 거죠.
export namespace StringType {
export type Split<T extends string, S extends string = ''> = T extends '' ? [] : string[];
}
먼저 T가 빈 문자인지를 확인하는 조건부타입을 추가하였습니다.
T 빈 문자라면 추론되는 결과는 `[]`라는 빈 배열이 나오고, 그렇지 않다면 `string []`이 나옵니다.
export namespace StringType {
export type Split<T extends string, S extends string = ''> = T extends '' ? [] : string[];
}
type test1 = StringType.Split<''> // []
type test2 = StringType.Split<'abcdefg', 'c'> // string[]
const test3 = ''.split(''); // string[]
String.prototype.split()은 빈 배열을 반환하더라도 추론 타입이 `string[]`으로 나옵니다.
이것만 하더라도 이미 프로토타입보다는 Type Safety 하다고 볼 수 있겠네요.
export namespace StringType {
export type Split<T extends string, S extends string = ''> = T extends ''
? []
: 빈문자가 아닐 경우에 대한 구현을 작성
}
이제 빈문자가 아닐 경우에 대한 구현을 작성해 봅시다.
우리는 들어있는 모든 S에 대하여 T를 분리해야 해야 합니다. 때문에 재귀적인 타입 추론이 사용됩니다.
앞에서부터 S를 기준으로 앞쪽에 위치한 문자열을 하나씩 분리해 배열 요소에 넣는 거죠.
export namespace StringType {
export type Split<T extends string, S extends string = ''> = T extends ''
? []
: T가 `${무엇인지 모르겠지만 S앞쪽에 있는 문자열1 입니다.}${S}${무엇인지 모르겠지만 S뒤쪽의 문자열2 입니다.}`
? 이라면 문자열1을 배열에 넣고, 문자열2에 대해 Split를 다시 실행합니다.
: 아니라면 더이상 나눌수 없다는 의미로 [T]를 반환합니다.
}
말로 풀어쓰면 위처럼 되겠네요. 위를 하나씩 코드로 바꿔 봅시다.
먼저 '무엇인지는 모르겠지만'은 infer 키워드를 사용해 나타낼 수 있습니다.
infer는 무조건 조건절 extends의 다음에 쓰이며 `만약의, 가상의 타입`을 의미합니다.
export namespace StringType {
export type Split<T extends string, S extends string = ''> = T extends ''
? []
: T extends `${infer Head}${S}${infer Tail}`
? 이라면 Head를 배열에 넣고, Tail(나머지 문자열)에 대해 Split를 다시 실행합니다.
: 아니라면 더이상 나눌수 없다는 의미로 [T]를 반환합니다.
}
infer는 어떤 타입이 올지 모르는 상황에서 유용합니다. 조건부타입에서 마치 지역변수처럼 사용되죠.
위 문장에서 `문자열 1을 infer Head`로 `문자열 2를 infer Tail`로 치환했습니다.
export namespace StringType {
export type Split<T extends string, S extends string = ''> = T extends ''
? []
: T extends `${infer Head}${S}${infer Tail}`
? [Head, ...Split<Tail,S>]
: [T]
}
재귀에 대한 구문을 추가하면 구현은 끝입니다.
전개연산자 `...`를 이용해 재귀에서 발생하는 배열의 중첩을 풀 수 있도록 했습니다.
`StringType.Split<'abcdf', 'c'>`의 작동예시를 들어볼까요.
- T는 ' abcdf'가 되고 S는 'c'가 됩니다.
- 빈 배열이 아니므로 T가 문자열 ${infer Head}${S}${infer Tail} 에 만족하는지 검사합니다.
- ${'ab'}${'c'}${'df'}으로 조건에 만족합니다. Head는 'ab'가 Tail은 'df'가 됩니다.
- ['ab', ...Split<'df', 'c'> ]가 반환값으로 설정되고, 재귀가 시작됩니다.
- T는 'df'가 되고 S는 'c'가 됩니다.
- 빈 배열이 아니므로 T가 문자열 ${infer Head}${S}${infer Tail} 에 만족하는지 검사합니다.
- 'df'에는 더 이상 'c'가 없기에 조건에 만족하지 않습니다.
- : [T]가 실행됩니다. ['df']가 반환값으로 설정되고 재귀가 끝납니다.
- 재귀 후 반환된 값은 ['ab', ...['df']] 입니다.
- 전개구문을 통해 배열이 전개됩니다.
- ['ab', 'df'] 타입이 반환됩니다.
아래 playground에서 테스트를 해볼 수 있습니다. ✌️
다음글에선 optional 파라미터 limit를 추가해 보도록 하겠습니다.
split(separator)
split(separator, limit)
감사합니다.
공부한 내용을 복습/기록하기 위해 작성한 글이므로 내용에 오류가 있을 수 있습니다.
'TS | NestJS' 카테고리의 다른 글
[TS] DeepMerge 타입 구현해보기 (0) | 2024.03.06 |
---|---|
[TS] 타입이 추론되는 String.prototype.split - 2 (0) | 2024.03.05 |
[TS] 유틸리티(Utility) 타입 (0) | 2024.03.02 |
[TS] TypeScript의 타입 조작 (제너릭, 조건부타입, infer) (0) | 2024.03.01 |
[TS] TypeScript의 기본 타입 (0) | 2024.02.29 |