티스토리 뷰
아래글에 이어 작성되었습니다.
Split 타입에 파라미터 limit를 추가합니다.
limit
String.prototype.split는 optional parameter로 limit를 전달할 수 있습니다.
/**
* 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[];
limit은 잘려진 요소 중 몇 개를 가져올 것인가를 의미합니다.
함수형에 익숙한 분이라면 이 매개변수를 `take`의 의미로 생각하셔도 좋을 것 같습니다.
우리가 원하는 타입은 limit의 기능과 같이, 숫자 리터럴 타입을 받아서 그만큼의 크기를 가진 배열이 나오길 바라는 겁니다.
/**
* 이렇게 타입이 추론되게 만들 수는 없을까?
*/
type Answer = StringType.Split<'abcdefg', '', 3>;// ["a", "b", "c"]
저는 Take이라는 새로운 제너릭 타입을 만들어서 이를 구현해 보려고 합니다.
Split로 문자열을 잘라 배열로 만들고, 만들어진 배열에서 Take을 이용해 원하는 개수만큼만 반환시키는 거죠.
Take 구현하기
Take는 고정된 길이를 가진 배열 타입을 받아서,
주어진 숫자만큼 앞에서부터 shift 한 결과를 담은 새 배열타입을 추론하는 타입입니다.
먼저 배열처리에 대한 새로운 네임스페이스 ArrayType를 정의합니다.
export namespace ArrayType {
}
Take 타입 파라미터 명시
앞에서 타입을 만드는 것은 함수를 만드는 것과 유사하다고 언급했었죠. 이어서 타입 파라미터를 정의해 봅시다.
export namespace ArrayType {
export type Take<
고정된-길이의-배열 extends any[],
주어진-숫자 extends number,
새로운-배열 extends any[] = [],
> = any;
}
Take는 `고정된 길이를 가진 배열 타입을 받아서`
`주어진 숫자만큼 앞에서부터 shift 한 결과를 담은 새 배열타입을 추론하는 타입`이니, 타입파라미터는 3개가 됩니다.
export namespace ArrayType {
export type Take<
T extends any[],
P extends number,
R extends any[] = [],
> = any;
}
문자로 치환에 보았습니다. 각 문자는 아래와 같이 사용되게 됩니다.
1. T라는 배열에서 shift 연산을 하여 R에 담아준다.
2. 하나의 요소를 담을 때마다, R의 length를 꺼내서 P에 도달했는지 확인한다.
3. 만약 R의 length가 P와 동일해진다면, 더 이상 옮기는 것을 멈추고 R을 추론 타입으로 반환한다.
이제 어떻게 요소를 옮길 것인지, 그리고 그 길이는 어떻게 파악할 것인지를 고민해 봐야겠네요.
ArrayType.Push<T, U> 추가
먼저 요소를 옮기는 타입을 만들어 보겠습니다.
shift 한 것을 배열의 끝에 넣어줘야 한다는 것이니, 이름은 push로 지어주었습니다.
export namespace ArrayType {
export type Take<
T extends any[],
P extends number,
R extends any[] = [],
> = any;
export type Push<T extends any[], U extends any> = [...T, U];
}
ArrayType.Push는 any[]로 추론되는 T를 받아, 새로 받은 타입 U를 끝에 넣어 새로운 배열 타입으로 반환합니다.
ArrayType.Length<T> 추가
이어서 배열의 길이를 추론하는 타입 Length<T>를 추가합니다.
export namespace ArrayType {
export type Take<
T extends any[],
P extends number,
R extends any[] = [],
> = any;
export type Length<T extends any[]> = T['length'];
export type Push<T extends any[], U extends any> = [...T, U];
}
`length`라는 인덱스 시그니처로 접근하여 숫자를 뽑기 때문에, 고정된 크기를 가진 배열이라면 길이가 추론됩니다.
Take <T, P, R> 구현
ArrayType.Take의 정의는 처음 말한 것처럼 R의 length가 P가 될 때까지 T의 요소를 하나씩 꺼내 옮기는 것입니다.
export namespace ArrayType {
export type Take<
T extends any[],
P extends number,
R extends any[] = [],
> = 만약 R의 length가 P라면
? 맞다면 R을 리턴한다.
: 그렇지 않다면, T가 첫번째 요소 F와 나머지 배열 타입 Rest로 이루어져 있는지 확인한다.
? 맞다면, R에 F를 담은 것을 새로운 R로 Take 타입을 재귀적으로 호출한다.
: 그렇지 않다면, 더 이상 배열에 아무것도 없다면 R이 추론되게 하고 재귀를 끝낸다.
export type Length<T extends any[]> = T['length'];
export type Push<T extends any[], U extends any> = [...T, U];
}
코드로 구현하면 다음과 같습니다.
export namespace ArrayType {
export type Take<
T extends any[],
P extends number,
R extends any[] = [],
> = ArrayType.Length<R> extends P
? R
: T extends [infer F, ...infer Rest]
? Take<Rest, P, ArrayType.Push<R, F>>
: R;
export type Length<T extends any[]> = T['length'];
export type Push<T extends any[], U extends any> = [...T, U];
}
R의 길이가 P와 같아질 때까지 T의 요소를 재귀적으로 R에 넣어주는 거죠.
StringType.Split에 ArrayType.Take 적용하기
Take가 구현되었으니, StringType.Split 타입파라미터 L을 추가하고 구현을 완성해 보겠습니다
먼저 기존의 StringType.Split의 이름은 StringType._Split으로 이름을 변경합니다.
`_`는 보통 private를 의미하죠. 저는 기존의 Split를 내부 타입으로 사용하기 위해 바꾸어 보았습니다.
export namespace StringType {
/**
* 타입의 이름을 `Split`에서 `_Split`으로 변경.
*/
export type _Split<T extends string, S extends string = ''> = T extends ''
? []
: T extends `${infer Head}${S}${infer Tail}`
? [Head, ..._Split<Tail,S>]
: [T]
export type Split<T extends string, S extends string = ''> = _Split<T,S>;
}
현재 Split은 받은 타입 파라미터를 그대로 내부 타입인 _Split으로 전달을 해주는 타입입니다.
이제 타입 파라미터 L을 추가해 보겠습니다.
타입 파라미터 L는 숫자를 입력받으며, 입력되지 않은 경우에는 자른 모든 배열 요소를 가지고 오도록 작동해야 합니다.
export namespace StringType {
/**
* 타입의 이름을 `Split`에서 `_Split`으로 변경.
*/
export type _Split<T extends string, S extends string = ''> = T extends ''
? []
: T extends `${infer Head}${S}${infer Tail}`
? [Head, ..._Split<Tail,S>]
: [T]
export type Split<T extends string, S extends string = '', L extends number = ArrayType.Length<_Split<T,S>>> = _Split<T,S>;
}
따라서 L 파라미터의 기본값은 가져올 수 있는 요소의 최대 개수, `ArrayType.Length<_Split<T,S>>`가 됩니다.
이제 L이 0이 아닌 경우, ArrayType.Take를 사용해서 L만큼 꺼내게 해 주면 타입 구현 완료입니다.
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]
export type Split<T extends string, S extends string = '', L extends number = ArrayType.Length<_Split<T,S>>> = L extends 0
? []
: ArrayType.Take<_Split<T,S>,L>;
}
타입 적용하기
새로운 프로토타입 객체인 StringPrototype을 만들고 그 안에 split이라는 함수를 선언합니다.
리턴 타입으로 지금까지 만든 `StringType.Split`을 넣어준 다음 함수에 대한 제네릭 타입 파라미터를 추가합니다.
그리고 그 타입들을 모두 리턴 타입의 제네릭으로 전달하고, 내부 로직은 원래 String.prototype.split을 적용합니다.
export const StringPrototype = {
/**
* type-safe split.
* @example StringPrototype.split('abcde', 'c', 1) // ['ab']
*
* @param original original string value.
* @param separator An object that can split a string.
* @param limit A value used to limit the number of elements returned in the array.
*
* @todo support `RegExp` as splitter
*/
split<T extends string, S extends string = '', L extends number = ArrayType.Length<StringType._Split<T,S>>>(
original: T,
separator?: S,
limit?: L,
): StringType.Split<T, S, L> {
return original.split(separator ?? '', limit) as StringType.Split<T, S, L>;
},
}
이제 메서드 호출 결과를 컴파일 시점에 미리 알 수 있는 함수 StringPrototype.split가 완성되었습니다! 🎉
아래 playground에서 전체코드와 테스트를 확인해 볼 수 있습니다.
감사합니다.
공부한 내용을 복습/기록하기 위해 작성한 글이므로 내용에 오류가 있을 수 있습니다.
'TS | NestJS' 카테고리의 다른 글
[NestJS] Swagger 적용하기 (feat. API 문서화) (0) | 2024.03.15 |
---|---|
[TS] DeepMerge 타입 구현해보기 (0) | 2024.03.06 |
[TS] 타입이 추론되는 String.prototype.split - 1 (0) | 2024.03.03 |
[TS] 유틸리티(Utility) 타입 (0) | 2024.03.02 |
[TS] TypeScript의 타입 조작 (제너릭, 조건부타입, infer) (0) | 2024.03.01 |