TS 挑战系列-easy

151 阅读5分钟

初衷:

github上很火的 Ts Type challenge系列,由于题目和答案分离,学习起来比较麻烦,所以在自己实践学习的过程中,把题目和答案整理在一起,并集合了每个题解的相关知识点。

适合人群

此文章适合有一定TS基础知识的人群,刚入门的那种就可以

Hellow World

// 期望是一个 string 类型
type HelloWorld = any

// 你需要使得如下这行不会抛出异常
type test = Expect<Equal<HelloWorld, string>>

// 答案
type HelloWorld = string

实现Pick

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}

// 答案
type MyPick<T, K extends keyof T> = {
   [key in K] : T[key] 
}

相关知识点

keyof 返回类型是联合类型

interface Person {
  name: string;
  age: number;
  location: string;
}

type K1 = keyof Person; // "name" | "age" | "location"

in 遍历属性

type Person = {
  name: string;
  age: number;
}

type TypeToNumber<T> = {
  [key in keyof T]: number
}

const obj: TypeToNumber<Person> = { name: 10, age: 10 }

[ 自定义变量名 in 枚举类型 ]: 类型

extends 在泛型场景里,约束左边必须是右边的子类型

function prop(obj: object, key: string) {
  return (obj as any)[key];
}

function prop<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
// 定义 T 类型并使用 extends 关键字约束该类型必须是 object 类型的子类型
// 然后使用 keyof 操作符获取 T 类型的所有键,其返回类型是联合类型
// 最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型

实现Readonly

// 题目
interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property

// 答案
type MyReadonly<T> = {
  readonly [key in keyof T]: T[key]
}

元组转换为对象

// 传入一个元组类型,将这个元组类型转换为对象类型,这个对象类型的键/值都是从元组中遍历出来

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

// typeof tuple 值为元祖类型
type result = TupleToObject<typeof tuple> 
// expected { tesla: 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

// 答案
type TupleToObject<T extends readonly any[]> = {
  [k in T[number]]: k
}

相关知识点

in 遍历元祖

const tuple = ['1', '2 ']
// T表示元祖 tuple
k in T[number]  // k对应指 1,2
// T[number] 表示元祖对应Key的值
// T[number] === keyof T

const 断言

  • 字面量类型使用const断言后,不能被修改
  • 对象字面量的属性使用后,将被readonly修饰,变成只读
  • 数组字面量变成readonly元祖

typeof

type Person = {
  name:string
  age:number
}
let man:Person = {
  name:'han'
  age:30
}
// 把man的type取出来,即Person
type Human = typeof man // Person

第一个元素 最后一个元素

实现一个通用First,它接受一个数组T并返回它的第一个元素的类型。

实现一个通用Last,它接受一个数组T并返回其最后一个元素的类型

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3

// 答案 
// 把不存在的情况,空数组置为 never
type First<T extends any[]> = T extends [] ? never : T[0]

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

相关知识点

infer 表示在extends条件语句中待推断的类型变量

type ParamType<T> = T extends (arg: infer P) => any ? P : T
// infer P 表示待推断的函数参数
// 整句表示为:如果 T 能赋值给 (arg: infer P) => any,
// 则结果是 (arg: infer P) => any 类型中的参数 P,否则返回为 T

type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any
// 用于提取函数类型的返回值类型
type Func = () => User;
type Test = ReturnType<Func>; // Test = User

获取元祖长度

// 创建一个通用的Length,接受一个readonly的数组,返回这个数组的长度。

type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']

type teslaLength = Length<tesla> // expected 4
type spaceXLength = Length<spaceX> // expected 5

// 答案
type Length<T extends readonly any[]> = T['length']

实现Exclude

从联合类型T中排除U的类型成员,来构造一个新的类型

type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

// 答案

// extends 作为条件判断
// 如果extends前面的类型能够赋值给extends后面的类型,那么表达式判断为真,否则为假
type MyExclude<T,U> = T extends U ? never : T

相关知识点

extends

type A1 = 'x' extends 'x' ? string : number; // string
type A2 = 'x' | 'y' extends 'x' ? string : number; // number
  
type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'> // string | number

为啥上面A1 和 A3是不同的结果?

答:分配条件类型:对于使用extends关键字的条件类型(即上面的三元表达式类型),如果extends前面的参数是一个泛型类型,当传入该参数的是联合类型,则使用分配律计算最终的结果。分配律是指,将联合类型的联合项拆成单项分别代入条件类型,然后将每个单项代入得到的结果再联合起来,得到最终的判断结果。

Includes

在类型系统里实现 JavaScript 的 Array.includes 方法,返回的类型要么是 true 要么是 false

type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> 
// expected to be `false`

// 答案

// 如果extends前面的类型能够赋值给extends后面的类型,那么表达式判断为真,否则为假
type Includes<T extends any[],U> = U extends T[number] ? true : false

Awaited

假如我们有一个 Promise 对象,这个 Promise 对象会返回一个类型。在 TS 中,我们用 Promise 中的 T 来描述这个 Promise 返回的类型。

请你实现一个类型,可以获取这个类型

type ExampleType = Promise<string>

type Result = MyAwaited<ExampleType> // string

// 答案
type MyAwaited<T> = T extends Promise<infer U> ?  MyAwaited<U> : T

If

// 类似于三元判断 true则返回左边
type A = If<true, 'a', 'b'>  // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'

// 答案
type If<C extends boolean,T,F> = C extends true ? T : F

Concat

type Result = Concat<[1], [2]> // expected to be [1, 2]

// 答案
type Concat<T extends any[],U extends any[]> = [...T,...U]

Push

type Result = Push<[1, 2], '3'> // [1, 2, '3']

// 答案
type Push<T extends any[],U> = [...T, U]

Unshift

type Result = Unshift<[1, 2], 0> // [0, 1, 2,]

// 答案
type Unshift<T extends any[],U> = [U,...T]

Parameters

const foo = (arg1: string, arg2: number): void => {}

type FunctionParamsType = MyParameters<typeof foo> 
// [arg1: string, arg2: number]

// 答案
type Func = (...args: any[]) => any
type MyParameters<T extends Func> = T extends (...args: infer R) => any ? R : never