美文网首页
TypeScript类型挑战

TypeScript类型挑战

作者: 胖乎乎的萝卜 | 来源:发表于2022-10-16 01:34 被阅读0次

    TIP: 原文发表于:cirplan.github.io

    前言

    在上一篇文章,初步介绍了 TS 的内置工具类型,让我们对 TS 的类型转换有了初步的认识。接下来为了加深我们的理解,我们动手进行一系列的挑战。

    这个挑战就是 type-challengesantfu 大佬创建的。再次膜拜顶级大佬。

    可以看到该挑战区分了 easymediumhardextreme 四大难度。点击进具体的题目,通过 Take a Challenge 进行作答,每一题会有对应的单元测试,测试通过代表你的解答是可以的。如果没有思路也没关系,可以通过 Check out Solutions 查看别人的思路。最后你有好的idea,可以 Share your Solutions,和大家一起讨论分享。

    准备

    在开始挑战之前,你或许需要了解一些常见的类型操作符。

    &

    交叉类型,把多个类型合并为一个类型。

    interface Colorful {
      color: string;
    }
    interface Circle {
      radius: number;
    }
     
    type ColorfulCircle = Colorful & Circle;
    // type ColorfulCircle = {
    //   color: string;
    //   radius: number;
    // }
    

    |

    联合类型,表示或的关系。

    function printId(id: number | string) {
      console.log("Your ID is: " + id);
    }
    // OK
    printId(101);
    // OK
    printId("202");
    // Error
    printId({ myID: 22342 });
    // Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
    

    Keyof

    对于对象类型,根据其 keys 返回一个字符串或数字的字面量联合类型。

    type Point = { x: number; y: number };
    type P = keyof Point; // 'x' | 'y'
    

    如果类型包含 字符串数字 索引,keyof 则直接返回其类型。

    type Arrayish = { [n: number]: unknown };
    type A = keyof Arrayish; // type A = number
    
    type Mapish = { [k: string]: boolean };
    type M = keyof Mapish; // type M = string | number
    

    M 的结果是 string | number 是因为 js 的对象索引都是转换成字符串的。所以 obj[0] 等同于 obj["0"]

    extends

    有条件的类型,其语法:

    T extends U ? X : Y
    

    意思是若 T 能够赋值给 U,那么类型是 X,否则为 Y

    让我们回顾下类型的分配规则。

    type-assignment.png

    行代表赋值的类型,列表示被赋值的类型。就像 any 能够分配给 unknowobjectvoidundefinednull 类型,但 never 不行。

    对于 T extends U ? X : Y,如果 T 的类型是 A | B | C,则会被解析为 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)

    infer

    extends 子语句中,可以使用 infer 进行类型推断,表示待推断的类型变量。这个变量可以在 true 分支中继续使用。

    type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
    
    type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
      ? Return
      : never;
     
    type Num = GetReturnType<() => number>; // type Num = number
     
    type Str = GetReturnType<(x: string) => string>; // type Str = string
     
    type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // type Bools = boolean[]
    

    好的,了解了以上操作符,也算入门了。快去尝试挑战吧。

    常见问题

    在我解答的过程中,有一些比较有意思的知识点,在这里分别说说。

    [T] extends [X] vs T extends X

    遇到这个问题是在 296・Permutation,你可以先尝试解答。

    T extends X 我们都知道,但为啥会有 [T] extends [X]?下面来看两个例子。

    type ToArray<Type> = Type extends any ? Type[] : never;
     
    type StrArrOrNumArr = ToArray<string | number>; // type StrArrOrNumArr = string[] | number[]
    
    type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
     
    // 'StrArrOrNumArr' is no longer a union.
    type StrArrOrNumArr = ToArrayNonDist<string | number>; // type StrArrOrNumArr = (string | number)[]
    

    简单的说,当 T 是联合类型的时候,会对其每个类型进行分配。由 string | number -> ToArray<string> | ToArray<number> -> string[] | number[]

    如果要避免这种行为,则可以通过添加 []extends 关键字两侧来限制。更具体可以看文档解析

    接下来再看两个例子,能够很好的证明上面的观点。

    type ToBooleanArr<T> = T extends boolean ? T[] : never;
    type BooleanArr = ToBooleanArr<boolean> // type BooleanArr = false[] | true[]
    
    type ToBooleanArr<T> = [T] extends [boolean] ? T[] : never;
    type BooleanArr = ToBooleanArr<boolean> // type BooleanArr = boolean[]
    

    看到这里你应该大概知道怎么回事了。接下来再看神奇的 never 判断。当我想要判断 T 是否是 never

    type CheckNotNever<T> = T extends never ? false : true;
    type CheckString = CheckNotNever<string> // type CheckString = true
    type CheckNever = CheckNotNever<never> // type CheckNever = never
    

    咦,发现没有,当传 never 的时候,CheckNever 预期应该是 false,结果返回 never。Why?

    我们再看一个测试:

    type UnionTypes = string | never | number // type UnionTypes = string | number
    

    UnionTypes 是不包含 never 的。所以通过上面两点,我们可以知道在 T extends never 时,如果 Tnever ,则表示空联合。那么当然不会执行后面的条件。

    所以要判断 never,我们只需要:

    type CheckNotNever<T> = [T] extends [never] ? false : true;
    type CheckString = CheckNotNever<string> // type CheckString = true
    type CheckNever = CheckNotNever<never> // type CheckNever = false
    

    对于上面这个问题,这里有个很好的解析,建议围观。

    K extends K

    这个判断会让人觉得很困惑,K 肯定是可以赋值给 K 的,那为啥还要判断?

    其实上面也有解析,当 K 是联合类型的时候,会对每个类型进行分配,其结果会再联合。这其实可以理解为一个hack,以进行循环。

    还是看这个解析,看完会有更全面的了解。

    type Permutation<T, K=T> =
        [T] extends [never]
          ? []
          : K extends K
            ? [K, ...Permutation<Exclude<T, K>>]
            : never
    
    iterate.png

    Object vs object vs {}

    这三个类型是 TS 中让人比较困惑的存在,先看几个例子:

    var o: object;
    o = { prop: 0 }; // OK
    o = []; // OK
    o = 42; // Error
    o = "string"; // Error
    o = false; // Error
    o = null; // Error
    o = undefined; // Error
    

    objectTS 2.2 版本新增的一种类型:表示非基础类型。即不是
    number | string | boolean | symbol | null | undefined 的类型。

    下面看 Object{}。这两个类型粗略一看一模一样啊,不是么?我们先看看 Object 的定义。

    其中 Object 接口定义了 Object.prototype 原型对象上的属性。ObjectConstructor 接口定义了 Object 类的属性。

    interface Object {
        /** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
        constructor: Function;
    
        /** Returns a string representation of an object. */
        toString(): string;
    
        /** Returns a date converted to a string using the current locale. */
        toLocaleString(): string;
    
        /** Returns the primitive value of the specified object. */
        valueOf(): Object;
    
        /**
         * Determines whether an object has a property with the specified name.
         * @param v A property name.
         */
        hasOwnProperty(v: PropertyKey): boolean;
    
        /**
         * Determines whether an object exists in another object's prototype chain.
         * @param v Another object whose prototype chain is to be checked.
         */
        isPrototypeOf(v: Object): boolean;
    
        /**
         * Determines whether a specified property is enumerable.
         * @param v A property name.
         */
        propertyIsEnumerable(v: PropertyKey): boolean;
    }
    
    interface ObjectConstructor {
        new(value?: any): Object;
        (): any;
        (value: any): any;
    
        /** A reference to the prototype for a class of objects. */
        readonly prototype: Object;
    
        /**
         * Returns the prototype of an object.
         * @param o The object that references the prototype.
         */
        getPrototypeOf(o: any): any;
    
        /**
         * Gets the own property descriptor of the specified object.
         * An own property descriptor is one that is defined directly on the object and is not inherited from the object's prototype.
         * @param o Object that contains the property.
         * @param p Name of the property.
         */
        getOwnPropertyDescriptor(o: any, p: PropertyKey): PropertyDescriptor | undefined;
    
        /**
         * Returns the names of the own properties of an object. The own properties of an object are those that are defined directly
         * on that object, and are not inherited from the object's prototype. The properties of an object include both fields (objects) and functions.
         * @param o Object that contains the own properties.
         */
        getOwnPropertyNames(o: any): string[];
    
        /**
         * Creates an object that has the specified prototype or that has null prototype.
         * @param o Object to use as a prototype. May be null.
         */
        create(o: object | null): any;
    
        /**
         * Creates an object that has the specified prototype, and that optionally contains specified properties.
         * @param o Object to use as a prototype. May be null
         * @param properties JavaScript object that contains one or more property descriptors.
         */
        create(o: object | null, properties: PropertyDescriptorMap & ThisType<any>): any;
    
        /**
         * Adds a property to an object, or modifies attributes of an existing property.
         * @param o Object on which to add or modify the property. This can be a native JavaScript object (that is, a user-defined object or a built in object) or a DOM object.
         * @param p The property name.
         * @param attributes Descriptor for the property. It can be for a data property or an accessor property.
         */
        defineProperty<T>(o: T, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): T;
    
        /**
         * Adds one or more properties to an object, and/or modifies attributes of existing properties.
         * @param o Object on which to add or modify the properties. This can be a native JavaScript object or a DOM object.
         * @param properties JavaScript object that contains one or more descriptor objects. Each descriptor object describes a data property or an accessor property.
         */
        defineProperties<T>(o: T, properties: PropertyDescriptorMap & ThisType<any>): T;
    
        /**
         * Prevents the modification of attributes of existing properties, and prevents the addition of new properties.
         * @param o Object on which to lock the attributes.
         */
        seal<T>(o: T): T;
    
        /**
         * Prevents the modification of existing property attributes and values, and prevents the addition of new properties.
         * @param a Object on which to lock the attributes.
         */
        freeze<T>(a: T[]): readonly T[];
    
        /**
         * Prevents the modification of existing property attributes and values, and prevents the addition of new properties.
         * @param f Object on which to lock the attributes.
         */
        freeze<T extends Function>(f: T): T;
    
        /**
         * Prevents the modification of existing property attributes and values, and prevents the addition of new properties.
         * @param o Object on which to lock the attributes.
         */
        freeze<T extends {[idx: string]: U | null | undefined | object}, U extends string | bigint | number | boolean | symbol>(o: T): Readonly<T>;
    
        /**
         * Prevents the modification of existing property attributes and values, and prevents the addition of new properties.
         * @param o Object on which to lock the attributes.
         */
        freeze<T>(o: T): Readonly<T>;
    
        /**
         * Prevents the addition of new properties to an object.
         * @param o Object to make non-extensible.
         */
        preventExtensions<T>(o: T): T;
    
        /**
         * Returns true if existing property attributes cannot be modified in an object and new properties cannot be added to the object.
         * @param o Object to test.
         */
        isSealed(o: any): boolean;
    
        /**
         * Returns true if existing property attributes and values cannot be modified in an object, and new properties cannot be added to the object.
         * @param o Object to test.
         */
        isFrozen(o: any): boolean;
    
        /**
         * Returns a value that indicates whether new properties can be added to an object.
         * @param o Object to test.
         */
        isExtensible(o: any): boolean;
    
        /**
         * Returns the names of the enumerable string properties and methods of an object.
         * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
         */
        keys(o: object): string[];
    }
    
    /**
     * Provides functionality common to all JavaScript objects.
     */
    declare var Object: ObjectConstructor;
    

    由于 js 的自动装箱特性,所以基础类型也是可以赋值给 Object{}

    
    var p: {}; // or Object
    p = { prop: 0 }; // OK
    p = []; // OK
    p = 42; // OK
    p = "string"; // OK
    p = false; // OK
    p = null; // Error
    p = undefined; // Error
    
    var q: { [key: string]: any };
    q = { prop: 0 }; // OK
    q = []; // OK
    q = 42; // Error
    q = "string"; // Error
    q = false; // Error
    q = null; // Error
    q = undefined; // Error
    
    var r: { [key: string]: string };
    r = { prop: 'string' }; // OK
    r = { prop: 0 }; // Error
    r = []; // Error
    r = 42; // Error
    r = "string"; // Error
    r = false; // Error
    r = null; // Error
    r = undefined; // Error
    

    由于 Object 是有原型方法的,如果实现其方法,则会进行对应校验。

    const obj1: Object = { 
       toString() { return 123 } // Error Type '() => number' is not assignable to type '() => string'. Type 'number' is not assignable to type 'string'.
    };
    

    但是 {} 则不一样。

    const obj1: {} = { 
       toString() { return 123 } // Success
    };
    

    结论:Object 是包含了 toStringhasOwnProperty 等方法的对象;{} 是空对象,但也能调用 Object 上的方法。可以说两者在运行时是没有差异的。但在编译时,{} 是不包含 Object 属性的,自然也不会进行校验。Object 则会进行对应校验,相比更加严格。

    更加具体也可以查看 stackoverflow 上的这个回答

    最后

    综上,希望能对你们了解 TS 类型系统有所帮助。同时,我把我的 type-challenges 解答整理在这里,有兴趣可以围观。

    参考

    相关文章

      网友评论

          本文标题:TypeScript类型挑战

          本文链接:https://www.haomeiwen.com/subject/yhakzrtx.html