type-challenges - medium
Get Return Type
できた
type MyReturnType<T> = T extends (...args: any) => infer R ? R : never
Omit
できなかった
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
}
Pickの反対で第二引数のプロパティ名をオブジェクトの型から除外する。
Pickは以下のような回答になるが、Mapped Typeの反復処理の中でKではない時だけ、追加するようにしたい。ここで何かしらの条件分岐が必要になりそうってところまではわかってるのだが。。。
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
Key Remapping In Mapped Types
and you can even filter out keys by producing never. That means you don’t have to use an extra Omit helper type in some cases.
条件でneverを返したものを除外できる
[P in keyof T
の部分で、T
の全てのプロパティに対する反復処理をしていてP
にプロパティ名が入る。
そしてそのプロパティ名P
に対してas
を用いて条件を指定。除外したい時にはnever
を返すことで最終的に戻すオブジェクトの型から対象のプロパティを除外することができる。
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
}
Readonly 2
できなかった
type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> &
Readonly<Pick<T, K>>;
参考にさせてもらった。
Intersection Types
interfaces allowed us to build up new types from other types by extending them. TypeScript provides another construct called intersection types that is mainly used to combine existing object types.
既存のオブジェクト型の組み合わせで新たな型を作成できる。交差型(?)
An intersection type is defined using the & operator.
&を使って定義する
e.g.
interface User {
id: string
name: string
age: number
}
interface Avatar {
src: string
size: 's' | 'm' | 'l'
}
// Type '{ id: string; name: string; age: number; src: string; }' is not assignable to type 'User & Avatar'.
Property 'size' is missing in type '{ id: string; name: string; age: number; src: string; }' but required in type 'Avatar'.
const userAvatar: User & Avatar = {
id: 'a',
name: 'a',
age: 18,
src: 'example.com',
}
OmitとPickの組み合わせでいけたかぁ。
Utility型使うってのがeasyの時にほぼなかったので、あ、そうか使っていいんだって感じ。
次の問題から、既存のUtility型使うってのも念頭に置いてやってみよ。
再掲
type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> &
Readonly<Pick<T, K>>;
-
Omit<T, K>
で第二引数のプロパティを第一引数の型から除外。 -
Readonly<Pick<T, K>>
で第二引数のプロパティをreadonly
にして追加。
default型について
<T, K extends keyof T = keyof T>
第二引数K
(readonlyにしたいプロパティ)が渡されなかった時には、すべてのT
のプロパティをreadonly
にしたいので、デフォルトで全てのプロパティを渡すための = keyof T
なるほどこんなことできたんだ。。
Deep Readonly
できなかった
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends Record<string, unknown>
? DeepReadonly<T[K]>
: T[K];
};
最初に普通のReadonlyを考えた
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}
やりたいことはT[K]
がオブジェクトならばDeepReadonlyを再帰で呼び出し、オブジェクトでない場合は、T[K]
返す
my 1st solution
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends {} ? DeepReadonly<T[K]> : T[K]
}
T[K] extends {}
がオブジェクトを判定できてなさそうな気がする
おんなじことやってる人いた。
extends {}
はtrue返すのでRecord
使うか、[k: string]: unknown
を使えと
Record
Constructs an object type whose property keys are Keys and whose property values are Type. This utility can be used to map the properties of a type to another type
プロパティにKeys
値にType
をもったオブジェクトの型を返す
e.g.
const someInfo: Record<'a' | 'b', string> = {
a: 'a',
b: 'b'
}
テストケース通んないぞ...?
Tuple to Union
できた
type TupleToUnion<T extends readonly any[]> = T[number]
Tupleの型が readonly any[]
で表現できることを覚えていたのでできた。
そして配列にしてしまえば インデックスとしてnumber
(型)を使えることも覚えていたのですんなりと解けた。
Chainable Options
Last of Array
できなかった
type Last<T extends any[]> = T extends [...any, infer Last] ? Last : never
Variadic Tuple Types
The second change is that rest elements can occur anywhere in a tuple - not just at the end!
TypeScript4.0による変更で、最後以外でも...
が使えるようになった
以前だったらこれができなかった
type Last<T extends any[]> = T extends [...any, infer Last] ? Last : never
...any
のところでA rest element must be last in a tuple type
Pop
できた
type Pop<T extends any[]> = T extends [...infer R, any] ? R : never
一つ前のLast of Arrayと同じ要領でVariadic Tuple Typesの4.0以降で可能になった機能を使って、撮りたいのは配列の最後の要素以外の要素なので
[...infer R, any]
最後の要素をanyとおき、それいがいをRとおく
infer
は条件部分でのみ使える.
Promise.all
できなかった
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{
[P in keyof T]: T[P] extends Promise<infer R> ? R : T[P]
}>
問題ちゃんと読めてなくて、最初こうかいてた
declare function PromiseAll<T extends readonly any[]>(values: T): T[number] extends Promise<infer R> ? R : T[number]
最終的に返すのはPromise<[型, 型, 型]>
memo
type A<T extends any[]> = [...T]
const a: A<['a', 1, 2, 3]> = ['a', 1, 2, 3]
[...T]
is ['a', 1, 2, 3]
ひとつひとつみていくと
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{
[P in keyof T]: T[P] extends Promise<infer R> ? R : T[P]
}>
- 引数の
values
をreadonly [...T]
と定義して、受け取った型(配列)Tを表現 - 最終的にPromiseを返すので戻りの型をPromiseでラップ
-
[P in keyof T]
で配列を反復処理 -
T[P] extends Promise<infer R>
で要素がPromiseであればその戻りの型を返す - そうでなければ、型そのまま返す
Type Lookup
できなかった
type LookUp<U, T> = U extends { type: T } ? U : never
このシンタックス忘れてて、U.typeとかU['type']とか試してた
U
が{ type: T }
に代入可能かっていうふうに思ってたけど、実際はU
は{ type: T }
を拡張しているか?っていうのが正しいのかな?
const dog: { type: 'dog' } = { type: 'dog', color: 'brown' }
これと同じことだと思うけど、代入可能ではないしな?
Uはユニオン型なので、Uの要素それぞれに対して反復処理で条件処理していく
あ、これってユニオンのUか。。。
Trim Left
できなかった
type space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${space}${infer L}` ? TrimLeft<L> : S
先頭が空白かジャッジして、空白ならTrimLeftを再帰的に呼び出す、でなければ返す。
まではわかったけれど、肝心のジャッジする部分がわからなかった。。
なるほどなああ
Template Literal Types
When used with concrete literal types, a template literal produces a new string literal type by concatenating the contents.
内容を連結して新しい文字列型を返す
type Color = 'red' | 'blue'
type BgColor = `bg_${Color}`
// type BgColor = "bg_red" | "bg_blue"
Trim
できた
type space = ' ' | '\n' | '\t'
type Trim<S extends string> = S extends `${space}${infer Right}` ? Trim<Right> : S extends `${infer Left}${space}` ? Trim<Left> : S
Trim Leftの要領でまず、先頭のスペースを処理していく。
先頭のスペースの処理が終わったら、末尾の処理をしていく、末尾も終われば最終的な文字列を返す
Capitalize
できた
type MyCapitalize<S extends string> = S extends `${infer First}${infer Rest}` ? `${Uppercase<First>}${Rest}` : ''
最初の文字を大文字にするのはUtility TypeのUppercase
でできるかなと、
あとはどう最初の文字を取り出すかで
infer
で最初の文字をFirstとおいてUppercase<First>
と残りの文字列をテンプレートリテラルで組み合わせた
Replace
できた
type Replace<S extends string, From extends string, To extends string> = From extends '' ? S : S extends `${infer Head}${From}${infer Tail}` ? `${Head}${To}${Tail}` : S
最初こう書いた
type Replace<S extends string, From extends string, To extends string> = S extends `${infer Head}${From}${infer Tail}` ? `${Head}${To}${Tail}` : S
するとFromが空文字のケースだけ失敗するので、最初にFromの文字列が空文字チェックして、空文字でなければ、最初に書いたのと同じようにテンプレートリテラルでFrom、To置き換えたものを返すようにした
ReplaceAll
できなかった
再帰の問題だと思ってこうかいた
type ReplaceAll<S extends string, From extends string, To extends string> = From extends '' ? S : S extends `${infer Head}${From}${infer Tail}` ? ReplaceAll<`${Head}${To}${Tail}`, From, To>: S
ただこれだと問題があって
ReplaceAll<'foobarfoobar', 'ob', 'b'>
のケースで
1回目のReplaceで fobarfobar
2回目のReplaceで fbarfbar
になってしまい、Replace後の文字列全てが毎回対象になってしまう
Replace後の文字列は対象に含めたくない。
Replace後の残りの文字列に対してだけ再帰的にReplaceAllしていく
type ReplaceAll<
S extends string,
From extends string,
To extends string
> = From extends ""
? S
: S extends `${infer Left}${From}${infer Right}`
? `${Left}${To}${ReplaceAll<Right, From, To>}`
: S;
Append Argument
できなかった
type AppendArgument<Fn, A> = Fn extends (...args: infer Args) => infer R ? (...args: [...Args, A]) => R : never
返す型の引数部分のシンタックスがわからなかった
type AppendArgument<Fn, A> = Fn extends (...args: infer Args) => infer R ? (xxx) => R : never
Length of String
できなかった
type LengthOfString<S extends string, A extends string[] = []> = S extends `${infer C}${infer T}` ? LengthOfString<T, [C, ...A]> : A['length']
最初に考えたのはS['length']
ただそこまでTypeScriptはうまく解釈してくれないみたいで、
単純にnumber typeを返してる. number literal type (0,1,2,3..)じゃなくて。
type LengthOfString<S extends string> = S['length']
const len: LengthOfString<''> = 1
これが通ってしまう。
最初のアプローチは上記と全く同じで、できなかったと。
次は、inferを使って最初の文字と、それ以外で分割して再帰的にLengthOfStringを呼び出していく。
カウンターをnumberで表現したいが型システムの中ではそれができず、A extends string[]
に呼び出されるたびに最初の文字をpushしていって、pushできる文字がなくなったら、Aは文字列の配列なので、A['length']
で要素数を返してあげる。
S extends
}
拡張可能なケース(三項演算子でtrueに入る)の境界がわからなかったので調べてみた。
なんか、いいデバッグ方法ないのかな...
type Test<S> = S extends `${infer Head}${infer Tail}` ? Tail : never
const test : Test<'aiueo'> = 'iueo' // ok
const test2 : Test<'a'> = '' // ok
const test3 : Test<''> = '' // ng - never
test: Head = 'a', Tail = 'iueo'
test2: Head = 'a', Tail = '' // still ok
test3 満たさない
ので、Sに1文字でも文字が渡ってくる間はtrueに入り続ける
Flatten
できなかった
type Flatten<T> = T extends []
? []
: T extends [infer H, ...infer Tail]
? [...Flatten<H>, ...Flatten<Tail>]
: [T];
Append to Object
type AppendToObject<T, U extends string, V> = {
[P in keyof T | U]: P extends keyof T ? T[P] : V
}
Absolute
できた
type Absolute<T extends number | string | bigint> = T extends number | bigint ? Absolute<`${T}`> : T extends `-${infer N}` ? N : T;
全ての引数をstringにして、先頭に-がついてればそれ以降を返す、ついてなければそのまま引数を返す。
- 全ての引数をstringに
T extends number | bigint ? Absolute<`${T}`>
2.先頭に-がついてればそれ以降を返す、ついてなければそのまま引数を返す
T extends `-${infer N}` ? N : T
stringに変換するところもテンプレートリテラル使えるのか。。スマート。。
type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer N}`
? N
: `${T}`;
String to Union
できなかった
type StringToUnion<T extends string> = T extends `${infer C}${infer Tail}` ? C | StringToUnion<Tail> : never
文字列Tをひとつずつ取り出し、再帰的に反復処理をしていくところ、空文字がきたらneverを返すってところまではわかったが、ユニオンを返すってところをどう書くのかがわからなかった。
アキュムレータ accumulatorを第二引数として準備しようとチャレンジしたが最終的にどうユニオンとして返すかわからなかった。
で文字列の後ろをStringToUnion
に渡して再帰的に反復処理をする。
最終的には C | never
となりnever
は無視される。ユニオンで反復処理の度、当該の文字が追加されていったユニオンを返す。
C | StringToUnion<T>
アキュムレータ accumulatorを第二引数として準備しようとチャレンジしたが最終的にどうユニオンとして返すかわからなかった。
これでできるみたい
type ArrayToUnion<A extends string[]> = A[number]
// Type '0' is not assignable to type '"a" | "b" | "c"'.(2322)
const a : ArrayToUnion<['a', 'b', 'c']> = 0
コメントにもあるが.
type StringToUnion<T extends string, A extends string[] = []> = T extends `${infer Head}${infer Tail}` ? StringToUnion<Tail, [Head,...A]>: A[number]
Merge
できた
type Merge<F, S> = {
[P in keyof F | keyof S]: P extends keyof S ? S[P] : P extends keyof F ? F[P] : never
}
Append to Objectと同じ要領で、2つの型のキーをユニオンにし、Mapped Typeで反復処理をしていく、キーP
がS
のキーだったら、S[P]
F
のキーだったらF[P]
を返す。
KebabCase
できなかった
type KebabCase<S> = S extends `${infer C}${infer T}`
? T extends Uncapitalize<T>
? `${Uncapitalize<C>}${KebabCase<T>}`
: `${Uncapitalize<C>}-${KebabCase<T>}`
: S;
Converts the first character in the string to a lowercase equivalent
与えられた型の最初の一文字を小文字にする
1 行目 ${infer C}${infer T}
頻出パターンで文字列の最初の一文字と、それ以降をinfering
2行目 2文字目以降の文字列の先頭が小文字であるかチェック
3行目 小文字であれば処理する必要なし、Tを新たな引数にKebabCaseを再帰呼び出し
4行目 小文字でない=大文字なので、ケバブケースへの変換が必要。Tの前に-
をいれてKebabCaseを再帰呼び出し。呼び出す時には、大文字のままだが、次の反復処理で、Uncapitalize<C>
される。
type KebabCase<S> = S extends `${infer C}${infer T}`
? T extends Uncapitalize<T>
? `${Uncapitalize<C>}${KebabCase<T>}`
: `${Uncapitalize<C>}-${KebabCase<T>}`
: S;
Diff
できなかった
ype Diff<O, O1> = {
[P in keyof O | keyof O1 as Exclude<P, keyof O & keyof O1>]: P extends keyof O
? O[P]
: P extends keyof O1
? O1[P]
: never
}
いつもお世話になっているこちらを参考にした
- [P in keyof O | keyof O1] でO、O1のそれぞれのオブジェクトのキーを全て取り出す
- P extends keyof Oで 対象のPがOのオブジェクトに含まれるのかチェック、O1も同様
- as Exclude<P, keyof 0 & keyof O1>でPはOにもO1にも含まれないキーだけになる
Exclude
久しぶり。第一引数のユニオン型から、第二引数のキー(メンバ?)を取り除いた型を返す。
Intersection Types
オブジェクト型同士を&でつなぐことで、両方のメンバー全てをもつことができる
Key Remapping in Mapped Types
このExampleがまんまそれ
KがUnionになって、'kind'を取り除いたUnionを返している
// Remove the 'kind' property
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
type KindlessCircle = {
radius: number;
}
この部分の、
as より前は、PはOとO1を含めた全てのオブジェクトキーを表現していて
as より後ろで、全てのオブジェクトキーのユニオンから、keyof O かつ keyof O1のキーを取り除いたユニオン型を返してる。
[P in keyof O | keyof O1 as Exclude<P, keyof O & keyof O1>]
AnyOf
できなかった
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest]
? First extends 0 | "" | false | [] | { [P in any]: never }
? AnyOf<[...Rest]>
: true
: false
ここまでできたが空のオブジェクトを型システム上で表現するのかがわからなかった。
- 配列の頭からループする
- 要素がFalsyか判定する
- Flasyならば残りの配列を引数とし再帰的に処理していく
- Trueならばtrue返す
- 配列がなくなったらtrueが含まれないのでfalseを返す
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest]
? First extends 0 | "" | false | [] | {}
? AnyOf<[...Rest]>
: true
: false
こちらのコメントでもディスカッションされていた。
{ [P in any]: never }
と表現できるらしい
isNever
できなかった
type IsNever<T> = [T] extends [never] ? true : false;
IsUnion
できなかった
type IsUnion<T, C = T> = T extends C ? ([C] extends [T] ? false : true) : never;
一番理解するのに時間がかかったかもしれない。。
TがUnion型であれば、conditionで分配法則が適用されるので、T extends Something
とすれば、TはUnion型の各要素(?)が一つずつ入ってくるようになる。
Distributive Conditional Types
おさらい。type ToArray<T> = T extends any ? T[] : never;
type SomeArray = ToArray<string | number>; // string[] | number[]
type IsUnion<T, C = T> = T extends C ? ([C] extends [T] ? false : true) : never;
回答見てもあんまぴんときてない。。
第二引数にTのコピーとしてのCを置いておく。
T extends Cが trueのブランチにはいるってことは string extends string | number のようになり、これは分配されているということになる?
Tがstring | numberの場合、[C] は [string | number]?
んで[T] は [string]、[number]か?
([string | number] extends [string] ? false : true)
こんな感じ?
ReplaceKeys
できなかった
type ReplaceKeys<U, T, Y> = {
[P in keyof U]: P extends T ? (P extends keyof Y ? Y[P] : never) : U[P]
}
どうやってユニオン型を返すのかってところがわからず。。
こちらの解説によると、
The same applies to mapped types. We can write a mapped type that iterates over the keys of type parameter and what actually happens is iterating over a single element of union, not the whole union.
オブジェクトのユニオンを受け取って、Mapped Typesでそれをループすると、ユニオンの要素の一つ一つに対して、 [P in keyof U]: P extends T ? (P extends keyof Y ? Y[P] : never) : U[P]
この処理をやっていく。らしい。
Remove Index Signature
できなかった
type TypeLiteralOnly<T> = string extends T
? never
: number extends T
? never
: symbol extends T
? never : T;
type RemoveIndexSignature<T> = { [P in keyof T as TypeLiteralOnly<P>]: T[P] };
You can filter out keys by producing never via a conditional type
オブジェクトの中から特定のキーをフィルタリングするときは、asを用いたkey remappingが使える。
Drop Char
久々できた
type DropChar<S, C extends string, R extends string = ''> = S extends `${infer Head}${infer Tail}`
? Head extends C
? DropChar<Tail, C, R>
: DropChar<Tail, C, `${R}${Head}`>
: R
- 最終的な戻りの型用の引数を用意する。
- 文字列の先頭から一つ一つ取り出していく
2-1. 文字列Cと一致したらdropしたいので、戻りの型Rに値をプッシュせず、次の文字列へ
2-2. 文字列Cと一致しなければ、戻りの型にプッシュしたいので、${現時点までの回答文字列}${現在処理中の文字列}
- チェックする文字列がなくなったら、回答用の型Rを返す。
Pick By Type
できた
type PickByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P]
}
最終的にフィルタリングしたオブジェクトを返すので、
Tのkeyをループさせ、keyのvalue(T[P])が extends Uでtrueのブランチに入るなら、そのプロパティは残して、そうでないなら、スキップしたい。
StartsWith
できた
type StartsWith<T extends string, U extends string> = U extends ''
? true
: T extends `${infer FH}${infer FT}`
? U extends `${infer SH}${infer ST}`
? FH extends SH
? StartsWith<FT, ST>
: false
: false
: false
めっちゃ簡単なソリューションあった。。
type StartsWith<T, U extends string> = T extends `${U}${any}`
? true
: false
EndsWith
できた
type EndsWith<T extends string, U extends string> = T extends `${any}${U}` ? true : false
StartsWith先にやってたので、パッとできた。
RequiredByKeys
できなかった
type Copy<T> = { [P in keyof T]: T[P] }
type RequiredByKeys<T, K = keyof T> = Copy<
{ [P in keyof T as P extends K ? never : P]: T[P]}
&
{ [P in keyof T as P extends K ? P : never]-?: T[P]}
>
Mutable
できた
type Mutable<T> = {
-readonly[P in keyof T]: T[P]
}
一つ前のRquiredByKeysでMapping Modifiersでてきたから、すんなり。
OmitByType
できた
type OmitByType<T, U> = {
[P in keyof T as T[P] extends U ? never : P]: T[P]
}
ObjectEntries
できなかった
type HandleUndefined<F, S extends keyof F> = F[S] extends infer R | undefined ? R : F[S]
type ObjectEntries<T> = {[P in keyof T]-?: [P, HandleUndefined<T, P>]}[keyof T]
一旦、オブジェクトを返して、それをユニオンにコンバートするっていう頭なかったな。。
{[P in keyof T]: [P, T[P]}
で { key: [key, keyof T, ... }になって
{}[keyof T]
で [key, keyof T] | ...になる
In our solution above, we are using the same trick. Since keyof T can be any of the keys present in T, typescript generates all possible outcomes and turns them into a union type.
Tが配列で T[number] やると、配列の要素のユニオンを返すのと同様にTがオブジェクトでT[keyof T] すると、Tのvalue (keyof T) のユニオンを返す。