DEV Community

tmhao2005
tmhao2005

Posted on Edited on

TypeScript useful advanced types

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

Overloading

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
Enter fullscreen mode Exit fullscreen mode

Strict union type


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
}
Enter fullscreen mode Exit fullscreen mode

Loop through an tuple array type

Link to here

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;
Enter fullscreen mode Exit fullscreen mode
  • Deep required
type DeepRequired<T> = {
  [K in keyof T]: Required<DeepRequired<T[K]>>;
};
Enter fullscreen mode Exit fullscreen mode
  • Make some props become required:
type RequiredProps<T extends object, K extends keyof T = keyof T> = Omit<T, K> & Required<Pick<T, K>>;
Enter fullscreen mode Exit fullscreen mode
  • Make some props become optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
Enter fullscreen mode Exit fullscreen mode

Recursively partial type

type RecursivePartial<T> = {
  [P in keyof T]?:
    T[P] extends (infer U)[] ? RecursivePartial<U>[] :
    T[P] extends object | undefined ? RecursivePartial<T[P]> :
    T[P];
};
Enter fullscreen mode Exit fullscreen mode
  • Array.prototype.map returns as const tuple:
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']
Enter fullscreen mode Exit fullscreen mode

Keep array as const with suggestion enabled:

type FooBar = "foo" | "bar";
// `foo` now has type as `readonly ["foo", "bar"]`
const foo = ["foo", "bar"] as const satisfies readonly FooBar[];
Enter fullscreen mode Exit fullscreen mode

Get all paths for all props:

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)];
Enter fullscreen mode Exit fullscreen mode

Check an arbitrary property exits in an generic object:

function hasOwnProperty<X extends Record<string, any>, Y extends PropertyKey>(
    obj: X,
    prop: Y
): obj is X & Record<Y, any> {
    return prop in obj;
}
Enter fullscreen mode Exit fullscreen mode

Split string with space as smaller strings

type Parts<Path> = Path extends `${infer PartA} ${infer PartB}` ? PartA | Parts<PartB> : Path;
// Test
type Test0 = Parts<'a b c'> // 'a' | 'b' | 'c'
Enter fullscreen mode Exit fullscreen mode

Is union type

type IsUnion<T, U extends T = T> = T extends unknown ? ([U] extends [T] ? false : true) : false;
Enter fullscreen mode Exit fullscreen mode

Is never type

type IsNever<T> = [T] extends [never] ? true : false;
// Test
type Test0 = IsNever<never> // true;
type Test0 = IsNever<string> // false;
Enter fullscreen mode Exit fullscreen mode

Is type an any?

type IsAny<T> = 0 extends 1 & T ? true : T;
// Test
type Test0 = IsAny<any> // true;
type Test1 = IsAny<string> // string;
Enter fullscreen mode Exit fullscreen mode

Is an empty object

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
Enter fullscreen mode Exit fullscreen mode

Known keys

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}
Enter fullscreen mode Exit fullscreen mode

Flatten object keys

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
Enter fullscreen mode Exit fullscreen mode

Deep exclude

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}
Enter fullscreen mode Exit fullscreen mode
  • Omit never values
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}
Enter fullscreen mode Exit fullscreen mode

Deep omit keys

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}}
Enter fullscreen mode Exit fullscreen mode
  • Pick keys which is a bit complicated than omit keys
// 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}}
Enter fullscreen mode Exit fullscreen mode

A type of mapping

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, {...});
Enter fullscreen mode Exit fullscreen mode

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

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
  }
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)