TypeScript Learning-10 Advanced Types

Posted by imderek on Fri, 04 Feb 2022 18:40:12 +0100

Advanced Types

Intersection Types

Cross-type combines multiple types into one type. This allows us to overlay existing types into one type, which contains all the required characteristics. For example, Person & Serializable & Loggable are both Person and Serializable and Loggable. This means that objects of this type have all three types of members at the same time.

Most of us see cross-type use in mixins or other places that are not appropriate for a typical object-oriented model. (There are many times this happens in JavaScript!) Here is a simple example of how to create a mix:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

class Person {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

Union Types

The union type is associated with the crossover type, but it is used quite differently. Occasionally you will encounter this situation, and a code base wants to pass in parameters of type number or string. For example, the following functions:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: any) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

padLeft("Hello world", 4); // returns "    Hello world"

One problem with padLeft is that the type of padding parameter is specified as any. That means we can pass in a parameter that is neither number ed nor string, but TypeScript does not error.

let indentedString = padLeft("Hello world", true); // Compile phase passed, runtime error

In traditional object-oriented languages, we may abstract these two types into hierarchical types. This is clearly clear, but there is also overdesign. One of the benefits of the original version of padLeft is that it allows us to pass in the original type. This is both simple and convenient to use. If we just want to use functions that already exist, this new approach won't work.

Instead of any, we can use the union type as a parameter for padding:

/**
 * Takes a string and adds "padding" to the left.
 * If 'padding' is a string, then 'padding' is appended to the left side.
 * If 'padding' is a number, then that number of spaces is added to the left side.
 */
function padLeft(value: string, padding: string | number) {
    // ...
}

let indentedString = padLeft("Hello world", true); // errors during compilation

The union type indicates that a value can be one of several types. We separate each type with a vertical line (|), so number | string | boolean means a value can be number, string, or boolean.

If a value is a union type, we can only access members common to all types of the union type.

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

The union type here may be a bit complex, but you can get used to it easily. If a value is of type A | B, we can be sure that it contains members that are common in A and B. In this example, Bird has a fly member. We are not sure if a variable of type Bird | Fish has a fly method. If the variable is of Fish type at run time, pet is called. Fly() makes a mistake.

Type Protection and Differentiating Types

The union type is appropriate for cases where values can be of different types. But what if we want to know exactly if it's Fish? A common way to distinguish between two possible values in JavaScript is to check if a member exists. As mentioned earlier, we can only access members that are jointly owned by a union type.

let pet = getSmallPet();

// Every member accesses with an error
if (pet.swim) {
    pet.swim();
}
else if (pet.fly) {
    pet.fly();
}

For this code to work, we'll use type assertions:

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

User-defined type protection

Notice here that we have to use type assertions many times. If we had checked the type, we would have clearly identified the type of pet in each subsequent branch.

The type protection mechanism in TypeScript makes it a reality. Type protection is expressions that are checked at run time to ensure the type in a scope. To define a type protection, we simply define a function whose return value is a type predicate:

function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
}

In this example, pet is Fish is a type predicate. The predicate is in the form of parameterName is Type, which must be a parameter name from the current function signature.

Whenever isFish is called with some variables, TypeScript reduces the variable to that specific type as long as it is compatible with the original type of the variable.

// 'swim'and'fly' calls are fine

if (isFish(pet)) {
    pet.swim();
}
else {
    pet.fly();
}

Note that TypeScript not only knows that pet is a Fish type in the if branch; It also knows that in the else branch, it must not be a Fish type, it must be a Bird type.

typeof type protection

Now let's go back and see how to write padLeft code using a union type. We can write with type assertions as follows:

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function isString(x: any): x is string {
    return typeof x === "string";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (isString(padding)) {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

However, it is painful to define a function to determine if the type is the original type. Fortunately, now we don't have to abstract typeof x == "number" as a function because TypeScript recognizes it as a type protection. That means we can check the type directly in the code.

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

These * typeof type protection * can only be recognized in two forms: typeof V == "typename" and typeof v!== "Typename", "typename must be" number","string","boolean"or"symbol". However, TypeScript does not prevent you from comparing to other strings, and languages do not recognize those expressions as type-protected.

instanceof type protection

If you have read typeof type protection and are familiar with the instanceof operator in JavaScript, you may have guessed what this section is about.

instanceof type protection is a way to refine types through constructors. For example, let's take a look at the previous string padding example:

interface Padder {
    getPaddingString(): string
}

class SpaceRepeatingPadder implements Padder {
    constructor(private numSpaces: number) { }
    getPaddingString() {
        return Array(this.numSpaces + 1).join(" ");
    }
}

class StringPadder implements Padder {
    constructor(private value: string) { }
    getPaddingString() {
        return this.value;
    }
}

function getRandomPadder() {
    return Math.random() < 0.5 ?
        new SpaceRepeatingPadder(4) :
        new StringPadder("  ");
}

// Type is SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();

if (padder instanceof SpaceRepeatingPadder) {
    padder; // Type refined to'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
    padder; // Type refined to'StringPadder'
}

The right side of instanceof requires a constructor, and TypeScript will be refined to:

  1. The type of the prototype property of this constructor, if it is not any type
  2. Construct the union of the types returned by the signature

In this order.

Types that can be null

TypeScript has two special types, null and undefined, which have values of null and undefined, respectively. We have outlined this in the section [Basic Types] (. /Basic Types.md). By default, the type checker assumes that null and undefined can be assigned to any type. Null and undefined are valid values for all other types. This also means that you can't stop assigning them to other types, even if you want to stop this. Tony Hoare, the inventor of null, called it a billion-dollar error.

The strictNullChecks tag resolves this error: when you declare a variable, it does not automatically contain null or undefined. You can explicitly include them using the union type:

let s = "foo";
s = null; // Error,'null'cannot be assigned to'string'
let sn: string | null = "bar";
sn = null; // Yes?

sn = undefined; // Error,'undefined'cannot be assigned to'string | null'

Note that according to JavaScript semantics, TypeScript treats null differently from undefined. string | null, string | undefined and string | undefined | null are different types.

Optional parameters and optional properties

With -strictNullChecks, optional parameters are automatically added with | undefined:

function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'

Optional attributes are treated the same way:

class C {
    a: number;
    b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'

Type protection and type assertions

Since types that can be null are implemented through union types, you need to use type protection to remove nulls. Fortunately, this is consistent with the code written in JavaScript:

function f(sn: string | null): string {
    if (sn == null) {
        return "default";
    }
    else {
        return sn;
    }
}

null is obviously removed here, and you can also use the short-circuit operator:

function f(sn: string | null): string {
    return sn || "default";
}

If the compiler cannot remove null or undefined, you can remove it manually using type assertions. Grammar is added! Suffix: identifier! Null and undefined are removed from the type of identifier:

function broken(name: string | null): string {
  function postfix(epithet: string) {
    return name.charAt(0) + '.  the ' + epithet; // error, 'name' is possibly null
  }
  name = name || "Bob";
  return postfix("great");
}

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

This example uses nested functions because the compiler cannot remove null s from nested functions unless they are function expressions that are called immediately. Because it cannot track all calls to nested functions, especially if you use an inner function as the return value of an outer function. If you don't know where the function was called, you don't know what type of name it was called with.

Type Alias

Type aliases give a type a new name. Type aliases are sometimes similar to interfaces, but they work on raw values, union types, tuples, and any other type you need to write by hand.

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}

An alias does not create a new type - it creates a new name to reference that type. Aliasing the original type is usually not useful, although it can be used as a form of documentation.

Like interfaces, type aliases can be generic - we can add type parameters and pass in to the right of the alias declaration:

type Container<T> = { value: T };

We can also use type aliases to refer to ourselves in attributes:

type Tree<T> = {
    value: T;
    left: Tree<T>;
    right: Tree<T>;
}

Together with the cross-type, we can create some very strange types.

type LinkedList<T> = T & { next: LinkedList<T> };

interface Person {
    name: string;
}

var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;

However, type aliases cannot appear anywhere on the right side of the declaration.

type Yikes = Array<Yikes>; // error

Interface vs. Type Alias

As we mentioned, type aliases can act like interfaces; However, there are some minor differences.

First, the interface creates a new name that can be used anywhere else. Type aliases do not create new names - for example, aliases are not used for error messages. In the following sample code, hover over an interfaced in the compiler to show that it returns an Interface, but hover over an aliased to show the object literal type.

type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

Another important difference is that type aliases cannot be extends and implementations (they cannot be extends and implements, either). Because objects in the software should be open to extensions, but closed to modifications, you should try to use interfaces instead of type aliases.

On the other hand, if you cannot describe a type through an interface and need to use a union or tuple type, a type alias is usually used.

String Literal Quantity Type

The string literal type allows you to specify a fixed value that a string must have. In practice, string literal type can work well with union type, type protection, and type aliases. By combining these features, you can implement strings of similar enumeration types.

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            // ...
        }
        else if (easing === "ease-out") {
        }
        else if (easing === "ease-in-out") {
        }
        else {
            // error! should not pass null or undefined.
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

You can only select one of the three allowable characters to pass as a parameter, and passing in other values will result in an error.

Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

The string literal type can also be used to distinguish function overloads:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}

Digital Literal Quantity Type

TypeScript also has a numeric literal type.

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
    // ...
}

We rarely use this directly, but they can be used when debugging bugs in a narrow range:

function foo(x: number) {
    if (x !== 1 || x !== 2) {
        //         ~~~~~~~
        // Operator '!==' cannot be applied to types '1' and '2'.
    }
}

In other words, when x is compared with 2, its value must be 1, which means that the comparison check above is illegal.

Enumerate member types

As mentioned in the Enumeration section, enumeration members are typed when each enumeration member is initialized literally.

When we talk about Singleton Types, most refer to enumerated member types and numeric/string literal types, although most users use Singleton Types and Literal Types interchangeably.

Discriminated Unions

You can combine singleton types, union types, type protection, and type aliases to create an advanced pattern called identifiable union, also known as label union or algebraic data type. Recognizable unions are useful in functional programming. Some languages automatically identify unions for you; TypeScript is based on existing JavaScript schemas. It has three elements:

  1. Has the common property of a single type - identifiable features.
  2. A type alias contains those types of unions-unions.
  3. Type protection on this property.
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}

First we declare the interfaces that will be federated. Each interface has a kind property but has a different string literal type. The kind attribute is called a recognizable feature or tag. Other properties are specific to each interface. Note that there are currently no connections between interfaces. Let's join them together:

type Shape = Square | Rectangle | Circle;

Now we use the identifiable union:

function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

Integrity check

When all changes to identifiable unions are not covered, we want the compiler to notify us. For example, if we add Triangle to Shape, we also need to update the area:

type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
    // should error here - we didn't handle case "triangle"
}

There are two ways to do this. The first is to enable - strictNullChecks and specify a return value type:

function area(s: Shape): number { // error: returns number | undefined
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

Because switch ing does not cover all cases, TypeScript assumes that this function sometimes returns undefined. If you explicitly specify that the return value type is number, you will see an error because the return value is actually of type number | undefined. However, this approach has some subtleties and - strictNullChecks does not support old code well.

The second method uses the never type, which the compiler uses for integrity checking:

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); // error here if there are missing cases
    }
}

Here, assertNever checks if s is of type never - the type that remains after all possible situations have been removed. If you forget a case, s will have a real type and you will get an error. This requires you to define an additional function, but it's more evident when you forget a case.

this type of polymorphism

The polymorphic this type represents a subtype that contains a class or interface. This is called the F-bounded polymorphism. It can easily represent inheritance between contiguous interfaces, for example. In the calculator example, this type is returned after each operation:

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    public multiply(operand: number): this {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}

let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();

Since this class uses this type, you can inherit it, and the new class can use the previous method directly without any changes.

class ScientificCalculator extends BasicCalculator {
    public constructor(value = 0) {
        super(value);
    }
    public sin() {
        this.value = Math.sin(this.value);
        return this;
    }
    // ... other operations go here ...
}

let v = new ScientificCalculator(2)
        .multiply(5)
        .sin()
        .add(1)
        .currentValue();

Without this type, ScientificCalculator cannot inherit Basic Calculator while maintaining interface coherence. Multiply returns BasicCalculator, which has no sin method. However, with this type, multiply returns this, in this case ScientificCalculator.

Index types

With index types, the compiler can check for code that uses dynamic property names. For example, a common JavaScript pattern is to select a subset of attributes from an object.

function pluck(o, names) {
    return names.map(n => o[n]);
}

The following is how to use this function in TypeScript to query and access operators by index type:

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Jarid',
    age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]

The compiler checks if name is really a property of Person. This example also introduces several new type operators. The first is keyof T, the index type query operator. For any type of T, keyof T results in a union of known public attribute names on T. For example:

let personProps: keyof Person; // 'name' | 'age'

Keyof Person is completely interchangeable with'name'|'age'. The difference is that if you add other attributes to Person, such as address: string, keyof Person automatically changes to'name'|'age' |'address'. You can use keyof in contexts like the pluck function, because you don't know what attribute names might appear before you use them. But the compiler checks if you passed in the correct attribute name to pluck:

pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'

The second operator is T[K], the index access operator. Here, the type syntax reflects the expression syntax. This means that person ['name'] has the type Person ['name'] - in our case, string. However, just like index type queries, you can use T[K] in normal context, which is where it's powerful. All you have to do is make sure the type variable K extends keyof T. For example, here is an example of the getProperty function:

function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

o: T and name: K in getProperty means o[name]: T[K]. When you return the result of T[K], the compiler instantiates the real type of the key, so the return value type of getProperty changes with the properties you need.

let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

Index type and string index signature

Keyof and T[K] interact with string index signatures. If you have a type with a string index signature, keyof T will be a string. And T[string] is the type of index signature:

interface Map<T> {
    [key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

mapping type

A common task is to make each attribute of a known type optional:

interface PersonPartial {
    name?: string;
    age?: number;
}

Or we want a read-only version:

interface PersonReadonly {
    readonly name: string;
    readonly age: number;
}

This is common in JavaScript, where TypeScript provides a way to create new types from old types - mapping types. In a mapping type, the new type converts each property of the old type in the same way. For example, you can make each property readonly or optional. Here are some examples:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}

Use as follows:

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

Let's look at the simplest mapping type and its components:

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

Its grammar and indexed signature grammar type, internally using for...in. There are three parts:

  1. Type variable K, which in turn binds to each property.
  2. Keys of a string literal union that contains a collection of attribute names to iterate over.
  3. The result type of the property.

In a simple example, Keys is a hard-coded list of attribute names and the attribute type is always boolean, so this mapping type is equivalent to:

type Flags = {
    option1: boolean;
    option2: boolean;
}

In a real application, it may be different than the Readonly or Partial above. They convert fields in a certain way based on some existing types. This is what keyof and index access types do:

type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

But what's more useful is that there are some generic versions available.

type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

In these examples, the attribute list is keyof T and the result type is a variant of T[P]. This is a good template for using generic mapping types. Because such transformations are homogeneous, the mapping only works on the attributes of T and nothing else. The compiler knows that all existing attribute modifiers can be copied before adding any new attributes. For example, suppose Person.name is read-only, then Partial.name will also be read-only and optional.

Here is another example where T[P] is packaged in the Proxy class:

type Proxy<T> = {
    get(): T;
    set(value: T): void;
}
type Proxify<T> = {
    [P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
   // ... wrap proxies ...
}
let proxyProps = proxify(props);

Note that Readonly and Artial are useful, so they are included with Pick and Record in the standard library for TypeScript:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string, T> = {
    [P in K]: T;
}

Readonly, Partial, and Pick are homologous, but Record is not. Because Record does not require an input type to copy attributes, it is not homomorphic:

type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

Non-homomorphic types essentially create new attributes, so they do not copy attribute modifiers from elsewhere.

Inferred by mapping type

Now that you know how to wrap a type of property, the next step is how to unpack it. It's also very easy:

function unproxify<T>(t: Proxify<T>): T {
    let result = {} as T;
    for (const k in t) {
        result[k] = t[k].get();
    }
    return result;
}

let originalProps = unproxify(proxyProps);

Note that this unpacking inference only applies to homomorphic mapping types. If the mapping type is not homologous, then you need to give the unpacking function a clear type parameter.

Predefined conditional types

TypeScript 2.8 in lib. Some predefined conditional types have been added to d.ts:

  • Exclude<T, U> - Excludes from T the types that can be assigned to U.
  • Extract<T, U> - Extracts the type in T that can be assigned to U.
  • NonNullable - null and undefined are excluded from T.
  • ReturnType - Gets the return value type of the function.
  • InstanceType - Gets the instance type of the constructor type.

Example

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(<T>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
type T17 = ReturnType<string>;  // Error
type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
type T23 = InstanceType<string>;  // Error
type T24 = InstanceType<Function>;  // Error
Be careful: Exclude Type is recommended Diff An implementation of the type. We use Exclude This name is defined to avoid damage Diff Code, and we feel that the name better conveys the semantics of the type. We did not increase Omit<T, K>Type because it's easy to use Pick<T, Exclude<keyof T, K>>To represent.

Topics: Javascript Front-end TypeScript