Open104

type-challenges - medium

tegurintegurin

Omit

https://github.com/type-challenges/type-challenges/blob/master/questions/3-medium-omit/README.md

できなかった

type MyOmit<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}
tegurintegurin

Pickの反対で第二引数のプロパティ名をオブジェクトの型から除外する。
Pickは以下のような回答になるが、Mapped Typeの反復処理の中でKではない時だけ、追加するようにしたい。ここで何かしらの条件分岐が必要になりそうってところまではわかってるのだが。。。

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P]
}
tegurintegurin

[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]
}
tegurintegurin

Readonly 2

https://github.com/type-challenges/type-challenges/blob/master/questions/8-medium-readonly-2/README.md

できなかった

type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> &
  Readonly<Pick<T, K>>;
tegurintegurin

Intersection Types

https://www.typescriptlang.org/docs/handbook/2/objects.html#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',
}
tegurintegurin

https://github.com/type-challenges/type-challenges/issues/7916

OmitとPickの組み合わせでいけたかぁ。
Utility型使うってのがeasyの時にほぼなかったので、あ、そうか使っていいんだって感じ。
次の問題から、既存のUtility型使うってのも念頭に置いてやってみよ。

再掲

type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> &
  Readonly<Pick<T, K>>;
  1. Omit<T, K>で第二引数のプロパティを第一引数の型から除外。
  2. Readonly<Pick<T, K>>で第二引数のプロパティをreadonlyにして追加。
tegurintegurin

default型について
<T, K extends keyof T = keyof T>
第二引数K(readonlyにしたいプロパティ)が渡されなかった時には、すべてのTのプロパティをreadonlyにしたいので、デフォルトで全てのプロパティを渡すための = keyof T

なるほどこんなことできたんだ。。

tegurintegurin

Deep Readonly

https://github.com/type-challenges/type-challenges/blob/master/questions/9-medium-deep-readonly/README.md

できなかった

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends Record<string, unknown>
    ? DeepReadonly<T[K]>
    : T[K];
};
tegurintegurin

最初に普通の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 {}がオブジェクトを判定できてなさそうな気がする

tegurintegurin

Last of Array

https://github.com/type-challenges/type-challenges/blob/master/questions/15-medium-last/README.md

できなかった

type Last<T extends any[]> = T extends [...any, infer Last] ? Last : never
tegurintegurin

Variadic Tuple Types

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#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

tegurintegurin

Pop

https://github.com/type-challenges/type-challenges/blob/master/questions/16-medium-pop/README.md

できた

type Pop<T extends any[]> = T extends [...infer R, any] ? R : never
tegurintegurin

Promise.all

https://github.com/type-challenges/type-challenges/blob/master/questions/20-medium-promise-all/README.md

できなかった

declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{
  [P in keyof T]: T[P] extends Promise<infer R> ? R : T[P]
}>
tegurintegurin

問題ちゃんと読めてなくて、最初こうかいてた

declare function PromiseAll<T extends readonly any[]>(values: T): T[number] extends Promise<infer R> ? R : T[number]

最終的に返すのはPromise<[型, 型, 型]>

tegurintegurin

memo

type A<T extends any[]> = [...T]
const a: A<['a', 1, 2, 3]> = ['a', 1, 2, 3]

[...T] is ['a', 1, 2, 3]

tegurintegurin

ひとつひとつみていくと

declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<{
  [P in keyof T]: T[P] extends Promise<infer R> ? R : T[P]
}>
  • 引数のvaluesreadonly [...T]と定義して、受け取った型(配列)Tを表現
  • 最終的にPromiseを返すので戻りの型をPromiseでラップ
  • [P in keyof T] で配列を反復処理
  • T[P] extends Promise<infer R>で要素がPromiseであればその戻りの型を返す
  • そうでなければ、型そのまま返す
tegurintegurin

Type Lookup

https://github.com/type-challenges/type-challenges/blob/master/questions/62-medium-type-lookup/README.md

できなかった

type LookUp<U, T> = U extends { type: T } ? U : never
tegurintegurin

このシンタックス忘れてて、U.typeとかU['type']とか試してた

U{ type: T } に代入可能かっていうふうに思ってたけど、実際はU{ type: T }を拡張しているか?っていうのが正しいのかな?

const dog: { type: 'dog' } = { type: 'dog', color: 'brown' }

これと同じことだと思うけど、代入可能ではないしな?

Uはユニオン型なので、Uの要素それぞれに対して反復処理で条件処理していく

あ、これってユニオンのUか。。。

tegurintegurin

Trim Left

https://github.com/type-challenges/type-challenges/blob/master/questions/106-medium-trimleft/README.md
できなかった

type space = ' ' | '\n' | '\t'
type TrimLeft<S extends string> = S extends `${space}${infer L}` ? TrimLeft<L> : S
tegurintegurin

Trim

https://github.com/type-challenges/type-challenges/blob/master/questions/108-medium-trim/README.md
できた

type space = ' ' | '\n' | '\t'

type Trim<S extends string> = S extends `${space}${infer Right}` ? Trim<Right> : S extends `${infer Left}${space}` ? Trim<Left> : S

tegurintegurin

Trim Leftの要領でまず、先頭のスペースを処理していく。
先頭のスペースの処理が終わったら、末尾の処理をしていく、末尾も終われば最終的な文字列を返す

tegurintegurin

Capitalize

https://github.com/type-challenges/type-challenges/blob/main/questions/00110-medium-capitalize/README.md

できた

type MyCapitalize<S extends string> = S extends `${infer First}${infer Rest}` ? `${Uppercase<First>}${Rest}` : ''
tegurintegurin

最初の文字を大文字にするのはUtility TypeのUppercaseでできるかなと、
あとはどう最初の文字を取り出すかで
inferで最初の文字をFirstとおいてUppercase<First>と残りの文字列をテンプレートリテラルで組み合わせた

tegurintegurin

Replace

https://github.com/type-challenges/type-challenges/blob/main/questions/00116-medium-replace/README.md

できた

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
tegurintegurin

最初こう書いた

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置き換えたものを返すようにした

tegurintegurin
tegurintegurin

再帰の問題だと思ってこうかいた

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後の文字列全てが毎回対象になってしまう

tegurintegurin

Replace後の文字列は対象に含めたくない。

https://github.com/type-challenges/type-challenges/issues/7972

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;
tegurintegurin

Length of String

https://github.com/type-challenges/type-challenges/blob/main/questions/00298-medium-length-of-string/README.md

できなかった

type LengthOfString<S extends string, A extends string[] = []> = S extends `${infer C}${infer T}` ? LengthOfString<T, [C, ...A]> : A['length']
tegurintegurin

最初に考えたのはS['length'] ただそこまでTypeScriptはうまく解釈してくれないみたいで、
単純にnumber typeを返してる. number literal type (0,1,2,3..)じゃなくて。

type LengthOfString<S extends string> = S['length']

const len: LengthOfString<''> = 1

これが通ってしまう。

tegurintegurin

https://ghaiklor.github.io/type-challenges-solutions/en/medium-length-of-string.html
最初のアプローチは上記と全く同じで、できなかったと。

次は、inferを使って最初の文字と、それ以外で分割して再帰的にLengthOfStringを呼び出していく。
カウンターをnumberで表現したいが型システムの中ではそれができず、A extends string[]に呼び出されるたびに最初の文字をpushしていって、pushできる文字がなくなったら、Aは文字列の配列なので、A['length']で要素数を返してあげる。

tegurintegurin

S extends {infer Head}{infer Tail}
拡張可能なケース(三項演算子で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に入り続ける

tegurintegurin

Flatten

できなかった
https://github.com/type-challenges/type-challenges/blob/main/questions/00459-medium-flatten/README.md

type Flatten<T> = T extends []
  ? []
  : T extends [infer H, ...infer Tail]
    ? [...Flatten<H>, ...Flatten<Tail>]
    : [T];
tegurintegurin

ここまでは考えた。 T extends [infer H, ...infer T] で配列かそうでないかを判定して、配列であればまた再帰的にFlattenを実行して、実際の要素に辿り着くまで繰り返す。

type Flatten<T> = T extends []
  ? []
  : T extends [infer H, ...infer T]
  ? xxx
  : xxx
tegurintegurin

Append to Object

https://ghaiklor.github.io/type-challenges-solutions/en/medium-append-to-object.html

type AppendToObject<T, U extends string, V> = {
  [P in keyof T | U]: P extends keyof T ? T[P] : V
}
tegurintegurin

そして、値の型はP extends keyof T なら 既存のオブジェクトのプロパティなので、T[P]として、プロパティの値の型を返す。そうでなければ新しいプロパティUなので、Uの型であるVを返す。

tegurintegurin

Absolute

https://github.com/type-challenges/type-challenges/blob/main/questions/00529-medium-absolute/README.md

できた

type Absolute<T extends number | string | bigint> = T extends number | bigint ? Absolute<`${T}`> : T extends `-${infer N}` ? N : T;
tegurintegurin

全ての引数をstringにして、先頭に-がついてればそれ以降を返す、ついてなければそのまま引数を返す。

  1. 全ての引数をstringに
T extends number | bigint ? Absolute<`${T}`>

2.先頭に-がついてればそれ以降を返す、ついてなければそのまま引数を返す

T extends `-${infer N}` ? N : T
tegurintegurin

String to Union

https://github.com/type-challenges/type-challenges/blob/main/questions/00531-medium-string-to-union/README.md
できなかった

type StringToUnion<T extends string> = T extends `${infer C}${infer Tail}` ? C | StringToUnion<Tail> : never
tegurintegurin

文字列Tをひとつずつ取り出し、再帰的に反復処理をしていくところ、空文字がきたらneverを返すってところまではわかったが、ユニオンを返すってところをどう書くのかがわからなかった。

アキュムレータ accumulatorを第二引数として準備しようとチャレンジしたが最終的にどうユニオンとして返すかわからなかった。

tegurintegurin

アキュムレータ 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
tegurintegurin

コメントにもあるが.

type StringToUnion<T extends string, A extends string[] = []> = T extends `${infer Head}${infer Tail}` ? StringToUnion<Tail, [Head,...A]>: A[number]
tegurintegurin

KebabCase

https://github.com/type-challenges/type-challenges/blob/main/questions/00612-medium-kebabcase/README.md

できなかった

type KebabCase<S> = S extends `${infer C}${infer T}`
  ? T extends Uncapitalize<T>
    ? `${Uncapitalize<C>}${KebabCase<T>}`
    : `${Uncapitalize<C>}-${KebabCase<T>}`
  : S;

tegurintegurin

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;

tegurintegurin

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
}
tegurintegurin
  1. [P in keyof O | keyof O1] でO、O1のそれぞれのオブジェクトのキーを全て取り出す
  2. P extends keyof Oで 対象のPがOのオブジェクトに含まれるのかチェック、O1も同様
  3. as Exclude<P, keyof 0 & keyof O1>でPはOにもO1にも含まれないキーだけになる
tegurintegurin

Key Remapping in Mapped Types

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#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;
}
tegurintegurin

この部分の、
as より前は、PはOとO1を含めた全てのオブジェクトキーを表現していて
as より後ろで、全てのオブジェクトキーのユニオンから、keyof O かつ keyof O1のキーを取り除いたユニオン型を返してる。

[P in keyof O | keyof O1 as Exclude<P, keyof O & keyof O1>]
tegurintegurin

AnyOf

https://github.com/type-challenges/type-challenges/blob/main/questions/00949-medium-anyof/README.md

できなかった

type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest] 
  ? First extends 0 | "" | false | [] | { [P in any]: never }
    ? AnyOf<[...Rest]>
    : true
  : false


tegurintegurin

ここまでできたが空のオブジェクトを型システム上で表現するのかがわからなかった。

  1. 配列の頭からループする
  2. 要素がFalsyか判定する
  3. Flasyならば残りの配列を引数とし再帰的に処理していく
  4. Trueならばtrue返す
  5. 配列がなくなったらtrueが含まれないのでfalseを返す
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest] 
  ? First extends 0 | "" | false | [] | {}
    ? AnyOf<[...Rest]>
    : true
  : false

tegurintegurin

isNever

https://github.com/type-challenges/type-challenges/blob/main/questions/01042-medium-isnever/README.md

できなかった

type IsNever<T> = [T] extends [never] ? true : false;

tegurintegurin

[]でラップすることで、直接neverと比較 (アサイン)せず、neverを含む[]として比較することでisNeverできる

tegurintegurin

IsUnion

https://github.com/type-challenges/type-challenges/blob/main/questions/01097-medium-isunion/README.md

できなかった

type IsUnion<T, C = T> = T extends C ? ([C] extends [T] ? false : true) : never;
tegurintegurin
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)
こんな感じ?

tegurintegurin

ReplaceKeys

https://github.com/type-challenges/type-challenges/blob/main/questions/01130-medium-replacekeys/README.md

できなかった

type ReplaceKeys<U, T, Y> = {
  [P in keyof U]: P extends T ? (P extends keyof Y ? Y[P] : never) : U[P]
}
tegurintegurin

どうやってユニオン型を返すのかってところがわからず。。

https://ghaiklor.github.io/type-challenges-solutions/en/medium-replacekeys.html
こちらの解説によると、

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]この処理をやっていく。らしい。

tegurintegurin

Remove Index Signature

https://github.com/type-challenges/type-challenges/blob/main/questions/01367-medium-remove-index-signature/README.md

できなかった

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] };
tegurintegurin

Drop Char

https://github.com/type-challenges/type-challenges/blob/main/questions/02070-medium-drop-char/README.md

久々できた

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
tegurintegurin
  1. 最終的な戻りの型用の引数を用意する。
  2. 文字列の先頭から一つ一つ取り出していく
    2-1. 文字列Cと一致したらdropしたいので、戻りの型Rに値をプッシュせず、次の文字列へ
    2-2. 文字列Cと一致しなければ、戻りの型にプッシュしたいので、${現時点までの回答文字列}${現在処理中の文字列}
  3. チェックする文字列がなくなったら、回答用の型Rを返す。
tegurintegurin

Pick By Type

https://github.com/type-challenges/type-challenges/blob/main/questions/02595-medium-pickbytype/README.md

できた

type PickByType<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P]
}
tegurintegurin

最終的にフィルタリングしたオブジェクトを返すので、
Tのkeyをループさせ、keyのvalue(T[P])が extends Uでtrueのブランチに入るなら、そのプロパティは残して、そうでないなら、スキップしたい。

tegurintegurin

StartsWith

https://github.com/type-challenges/type-challenges/blob/main/questions/02688-medium-startswith/README.md

できた

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

tegurintegurin

RequiredByKeys

https://github.com/type-challenges/type-challenges/blob/main/questions/02759-medium-requiredbykeys/README.md

できなかった

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]}
>

tegurintegurin

ObjectEntries

https://github.com/type-challenges/type-challenges/blob/main/questions/02946-medium-objectentries/README.md

できなかった

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]


tegurintegurin

https://ghaiklor.github.io/type-challenges-solutions/en/medium-object-entries.html

一旦、オブジェクトを返して、それをユニオンにコンバートするっていう頭なかったな。。

{[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) のユニオンを返す。