TypeScript useful advanced types
Tags: typescript
Published: 2022-11-29 04:13:22

As the title says, here are all the useful types that I'm using every day or create new types on top of them. I thought it might be handy for some people so I just share here and this will be updated moving forward.

### More resources
- https://github.com/piotrwitek/utility-types/tree/master?tab=readme-ov-file#table-of-contents
- https://github.com/millsp/ts-toolbelt

### Overloading

```ts
type Foo = (a: string) => string;
type Bar = (a: number) => number;

// it becomes overloading
declare let fn: Foo & Bar;

const x = fn(2); // nubmer
const y = fn("hello"); // string
```

### Strict union type
```ts

type A<T> = {
    isMulti: false;
    foo: string;
    onChange: (value: T) => any;
};
type B<T> = {
    isMulti: true;
    foo: number;
    onChange: (value: T[]) => any;
};
type C<T> = {
    foo: string;
    onChange: (value: T) => any;
};

// NOTE: this one only works well in case of `strict` mode on
// https://stackoverflow.com/questions/65805600/struggling-with-building-a-type-in-ts#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any
    ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>>
    : never;
type StrictUnion<T> = StrictUnionHelper<T, T>;

type Unions<T> = StrictUnion<A<T> | B<T> | C<T>>;

type Options<T = any> = {
    options: T[];
} & Unions<T>;

interface Option {
    value: string;
}

const a: Options<Option> = {
    options: [
        {
            value: "abc"
        }
    ],
    // trick is here
    onChange: <T extends Option>(value: T) => {
        return value;
    }
};

// test
if (a.isMulti) {
    a.onChange();
}
// unknown
const b = {
    // ...
} as Unions<Option>;

if (b.isMulti) {
    b.foo; // number
} else {
    b.foo; // string
}
```

### Loop through an tuple array type
[Link to here](#loop-through-an-tuple-array-type)
```ts
type Foo = [
  {
    id: number;
    items: [
  }
]
type ReduceItems<Arr extends ReadonlyArray<any>, Result extends any[] = []> = Arr extends []
    ? Result
    : Arr extends [infer H]
    ? H extends {items: ReadonlyArray<MenuItem>}
        ? [...Result, ...H["items"]]
        : never
    : Arr extends readonly [infer H, ...infer Tail]
    ? Tail extends ReadonlyArray<any>
        ? H extends {items: ReadonlyArray<MenuItem>}
            ? ReduceItems<Tail, [...Result, ...H["items"]]>
            : never
        : never
    : never;
```

- Deep required
```ts
type DeepRequired<T> = {
  [K in keyof T]: Required<DeepRequired<T[K]>>;
};
```

- Make some props become required:
```ts
type RequiredProps<T extends object, K extends keyof T = keyof T> = Omit<T, K> & Required<Pick<T, K>>;
```

- Make some props become optional
```ts
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
```

### Recursively partial type
```ts
type RecursivePartial<T> = {
  [P in keyof T]?:
    T[P] extends (infer U)[] ? RecursivePartial<U>[] :
    T[P] extends object | undefined ? RecursivePartial<T[P]> :
    T[P];
};
```

- `Array.prototype.map` returns as const tuple:

```ts
function map<T extends any[]>(
  // arr: T
  arr: readonly [...T]
): [...{ [I in keyof T]: `foo${T[I]}`}] {
  // 
  return arr.map(elem => `foo${elem}`) as any;
}

const arr = ['x', 'y'] as const satisfies readonly ('x' | 'y' | 'z')[]
const result = map(arr) // ['foox', 'fooy']
```

### Keep array `as const` with suggestion enabled:

```ts
type FooBar = "foo" | "bar";
// `foo` now has type as `readonly ["foo", "bar"]`
const foo = ["foo", "bar"] as const satisfies readonly FooBar[];
```

### Get all paths for all props: 

```ts
type NestedProps<T extends object> = {
    [P in keyof T & (string | number)]: T[P] extends Date
        ? `${P}`
        : T[P] extends Record<string, unknown>
        ? `${P}` | `${P}.${NestedProps<T[P]>}`
        : `${P}`;
}[keyof T & (string | number)];
```

### Check an arbitrary property exits in an generic object:
```ts
function hasOwnProperty<X extends Record<string, any>, Y extends PropertyKey>(
    obj: X,
    prop: Y
): obj is X & Record<Y, any> {
    return prop in obj;
}
```

### Split string with space as smaller strings
```ts
type Parts<Path> = Path extends `${infer PartA} ${infer PartB}` ? PartA | Parts<PartB> : Path;
// Test
type Test0 = Parts<'a b c'> // 'a' | 'b' | 'c'
```

### Is union type
```ts
type IsUnion<T, U extends T = T> = T extends unknown ? ([U] extends [T] ? false : true) : false;
```

### Is never type
```ts
type IsNever<T> = [T] extends [never] ? true : false;
// Test
type Test0 = IsNever<never> // true;
type Test0 = IsNever<string> // false;
```

### Is type an `any`?
```ts
type IsAny<T> = 0 extends 1 & T ? true : T;
// Test
type Test0 = IsAny<any> // true;
type Test1 = IsAny<string> // string;
```

### Is an empty object
```ts
type IsEmptyObject<T extends Record<PropertyKey, unknown>> =
  [keyof T] extends [never]
    ? true
    : false;
// Test
type Test0 = IsEmptyObject<{}> // true
type Test1 = IsEmptyObject<{foo: string}> // false
type Test2 = IsEmptyObject<{[K: string]: any}> // false
```

### Known keys
```ts
type KnownKeys<T> = {
  [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K];
}

// Test
type Test0 = KnownKeys<
  {foo: string; [K: string]: any} // index signature 
>; // {foo: string}
```

### Flatten object keys
```ts
type FlattenObjectKeys<
  T extends Record<string, unknown>,
  Key = keyof T
> = Key extends string
  ? T[Key] extends Record<string, unknown>
    ? `${Key}.${FlattenObjectKeys<T[Key]>}`
    : `${Key}`
  : never

// Test
type Test0 = FlattenObjectKeys<{foo: {bar: string}}> // foo | foo.bar
```

### Deep exclude

```ts
type DeepExclude<T, U> = T extends U
  ? never
  : T extends object
  ? {
      [K in keyof T]: DeepExclude<T[K], U>;
    }
  : T;
// Test
type Test0 = DeepExclude<
  {foo: string, bar: number}, 
  {foo: string}
> // {bar: number}
```
- Omit never values
```ts
type OmitNever<T> = Omit<
  T,
  {
    [K in keyof T]-?: IsNever<T[K]> extends true ? K : never;
  }[keyof T]
>;

// Test
type Test0 = OmitNever<{foo: string; bar: never}> // {foo: string}
```

### Deep omit keys
```ts
type OmitKeysRecursively<T, K extends PropertyKey> = Omit<
  {
    [P in keyof T]: T[P] extends object ? OmitKeysRecursively<T[P], K> : T[P];
  },
  K
>;
// Test
type Test0 = OmitKeysRecursively<
  {foo: string; bar: {baz: number, biz: boolean}},
  'foo' | 'baz'
> // {foo: string; bar: {biz: boolean}}
```

- Pick keys which is a bit complicated than omit keys
```ts
// helper helps remove keys with `never` as value
type OmitNever<T> = Omit<
  T,
  {
    [K in keyof T]-?: Pick<T, K> extends Partial<Record<K, undefined>> ? K : never;
  }[keyof T]
>;

type PickRecursively<T extends object, K extends PropertyKey> = OmitNever<{
  [P in keyof T]: P extends K
    ? T[P]
    : T[P] extends infer O
    ? O extends object
      ? PickRecursively<O, K>
      : never
    : never;
}> extends infer O
  ? { [P in keyof O]: O[P] }
  : never;

// Test
type TestP0 = PickRecursively<
  { foo: string; bar: { baz: string; bix: string } }, 
  'foo' | 'bar'
>; // {foo: string; bar: {baz: string}}
```

### A type of mapping
```ts
export enum Group {
  FOO = 'foo',
  BAR = 'bar',
}

interface Mapping {
  [Group.FOO]: { fooString: string; fooNumber: number };
  [Group.BAR]: { barString: string; barDate: Date; notFoo: string };
}

type Values<T> = T[keyof T]
type ParamAsArray<T> = Values<{
  [P in keyof T]: [P, T[P]]
}>

function method(...[p0, p1]: ParamAsArray<Mapping>) {...}
// call the method as normal
method(Group.FOO, {...});
```

### An another example of checking others props type based on an particular prop:

```ts
type Values<T> = T[keyof T];
type Props = Values<{
  [P in keyof JSX.IntrinsicElements]: { as: P } & {
    [K in keyof JSX.IntrinsicElements[P]]: JSX.IntrinsicElements[P][K];
  };
}>;

// Do not spread the props directly on parameter
// like: `{as, ...rest}`
// since TS is now not dynamic enough to update
// `rest` with correct type based on `as`
// but `rest` will contain all common props
const BaseComponent: React.FC<Props> = (props) => {
  if (props.as === 'a') {
    const { as, ...others } = props;
    console.log(others.href) // ok
  }
};
```