类型体操 | typescript炫技 | 练习题

本文探讨了 TypeScript 的高级类型操作,如TwoSum类型定义、数组操作、元组转换、泛型工具如SetOptional和ConditionalPick,以及函数重载和数组扁平化。通过实例展示了如何解决编译器问题和创建实用的工具类型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

类型体操

两数之和

包括定义加法 减法 获取数组除了第一个元素尾部 数组转联合类型

// ts => LeetCode第一题: 两数之和
type TwoSum<A extends number[], T extends number> = 
  A['length'] extends 0
    ? false
    : Sub<T, A[0]> extends Arr2Union<Tail<A>>
      ? true
      : TwoSum<Tail<A>, T>

// 生成长度为 N 的数组Arr
type toArray<L, Arr extends any[] = []> = 
  Arr['length'] extends L
    ? Arr
    : toArray<L, [...Arr, any]>

type Add<T extends number, K extends number> = 
  [...toArray<T>, ...toArray<K>]['length']

type Sub<T extends number, K extends number> = 
  toArray<T> extends [...toArray<K>, ...infer R]
    ? R['length']
    : never

type Tail<N extends any[]> = N extends [infer F, ...infer Tail] ? Tail : []

type Arr2Union<T extends any[]> = T[number]

type res = TwoSum<[1,2,3], 5>

获取元祖第一个元素

type First<T extends any[]> = T extends [infer First, ...infer Tail] ? First : never

元组转对象

type TupleToObject<T extends readonly any[]> = {
  [K in T[number]]: K
}

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

type cases = [
  Expect<Equal<TupleToObject<typeof tuple>, { tesla: 'tesla'; 'model 3': 'model 3'; 'model X': 'model X'; 'model Y': 'model Y' }>>,
]

类型体操练习题系列(持续更新)

1

type User = {
  id: number;
  kind: string;
};

function makeCustomer<T extends User>(u: T): T {
  // Error(TS 编译器版本:v4.4.2)
  // Type '{ id: number; kind: string; }' is not assignable to type 'T'.
  // '{ id: number; kind: string; }' is assignable to the constraint of type 'T', 
  // but 'T' could be instantiated with a different subtype of constraint 'User'.
  return {
    id: u.id,
    kind: 'customer'
  }
}


// 解决
function makeCustomer<T extends User>(u: T): T {
  return {
    ...u,
    id: u.id,
    kind: 'customer'
  }
}

2

我们希望参数 a 和 b 的类型都是一致的,即 a 和 b 同时为 number 或 string 类型。当它们的类型不一致的值,TS 类型检查器能自动提示对应的错误信息。

function f(a: string | number, b: string | number) {
  if (typeof a === 'string') {
    return a + ':' + b; // no error but b can be number!
  } else {
    return a + b; // error as b can be number | string
  }
}

f(2, 3); // Ok
f(1, 'a'); // Error
f('a', 2); // Error
f('a', 'b') // Ok



// 解决
// 函数重载
function f(a: string, b: string): string
function f(a: number, b: number): number
function f(a: string | number, b: string | number) {
  if (typeof a === 'string' || typeof b === 'string') {
    return a + ':' + b;
  } else {
    return a + b
  }
}

f(2, 3); // Ok
f(1, 'a'); // Error
f('a', 2); // Error
f('a', 'b') // Ok

3

TS 内置的工具类型:Partial,它的作用是将某个类型里的属性全部变为可选项

// lib.es5.d.ts
type Partial<T> = {
  [P in keyof T]?: T[P];
};

那么如何定义一个 SetOptional 工具类型,支持把给定的 keys 对应的属性变成可选的
对应的使用示例如下所示:

type Foo = {
	a: number;
	b?: string;
	c: boolean;
}

// 测试用例
type SomeOptional = SetOptional<Foo, 'a' | 'b'>;

// type SomeOptional = {
// 	a?: number; // 该属性已变成可选的
// 	b?: string; // 保持不变
// 	c: boolean; 
// }

在实现 SetOptional 工具类型之后,如果你感兴趣,可以继续实现 SetRequired 工具类型,利用它可以把指定的 keys 对应的属性变成必填的。对应的使用示例如下所示:

type Foo = {
	a?: number;
	b: string;
	c?: boolean;
}

// 测试用例
type SomeRequired = SetRequired<Foo, 'b' | 'c'>;
// type SomeRequired = {
// 	a?: number;
// 	b: string; // 保持不变
// 	c: boolean; // 该属性已变成必填
// }

答案SetOptional

type Foo = {
	a: number;
	b?: string;
	c: boolean;
}

type Simplify<T> = {
  [P in keyof T]: T[P]
}// 用来对交叉类型进行扁平化处理

type SetOptional<T, K extends keyof T> =
  Simplify<Partial<Pick<T, K>> & Pick<T, Exclude<keyof T, K>>>

  // Simplify<
  //   Partial<Pick<T, K>>  /* Partial<A>作用是把 A 所有类型变为可选 这里的A是后面从T中挑出所有属于K*/
  //   & 
  //   Pick<T, Exclude<keyof T, K>> /* 从T中挑出不属于K的 这部分不需要变成可选*/
  // >
  // Simplify作用是把这个交叉类型整合到一起

type SomeOptional = SetOptional<Foo, 'a' | 'b'>;

const obj: SomeOptional = {
	c: false
}

答案SomeRequired

// 同上
type SomeRequired = 
  Simplify<Required<Pick<T, K>> & Pick<T, Exclude<keyof T, K>>>

4

Pick<T, K extends keyof T> 的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。

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

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false
};
那么如何定义一个 ConditionalPick 工具类型,支持根据指定的 Condition 条件来生成新的类型,对应的使用示例如下:

interface Example {
	a: string;
	b: string | number;
	c: () => void;
	d: {};
}

// 测试用例:
type StringKeysOnly = ConditionalPick<Example, string>;
// => {a: string}

// 答案
type ConditionalPick<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
}
// as 我理解成等于号 = ,后面是hi三目运算
// 如果K in keyof T  获取T所有键 并遍历
// 如果T[K]即属性的value值 符合V的约束的挑选出来
// 不符合的杀掉 变成never

5

定义一个工具类型 AppendArgument,为已有的函数类型增加指定类型的参数,新增的参数名是 x,将作为新函数类型的第一个参数。具体的使用示例如下所示:

type Fn = (a: number, b: string) => number
type AppendArgument<F, A> = // 你的实现方式

type AppendArgument<F extends (...args: any) => any, A> 
  = (x: A, ...args: Parameters<F>) => ReturnType<F> 

type AppendArgument<F, A> = F extends (...args: infer Args) => infer Return ? 
  (x: A, ...args: Args) => Return : never

type FinalFn = AppendArgument<Fn, boolean> 
// (x: boolean, a: number, b: string) => number

6

定义一个 NativeFlat 工具类型,支持把数组类型拍平(扁平化)。具体的使用示例如下所示:

type NaiveFlat<T extends any[]> = {
  [P in keyof T]: T[P] extends any[] ? T[P][number] : T[P]
}[number]

// 测试用例:
type NaiveResult = NaiveFlat<[['a'], ['b', 'c'], ['d']]>
// NaiveResult的结果: "a" | "b" | "c" | "d"
在完成 NaiveFlat 工具类型之后,在继续实现 DeepFlat 工具类型,以支持多维数组类型:

type DeepFlat<T extends any[]> = {
  [K in keyof T]: T[K] extends any[] ? DeepFlat<T[K]> : T[K]
}[number]

// 测试用例
type Deep = [['a'], ['b', 'c'], [['d']], [[[['e']]]]];
type DeepTestResult = DeepFlat<Deep>  
// DeepTestResult: "a" | "b" | "c" | "d" | "e"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值