Explicitly typing an object with TypeScript

后端 未结 2 1851
小鲜肉
小鲜肉 2020-12-20 02:37

I\'m working on converting my little library from JavaScript to TypeScript, and I have a function there

function create(declarations: Declarations) {
         


        
相关标签:
2条回答
  • 2020-12-20 03:34

    It seems this is not possible to do, in the TypeScript Deep Dive book there's a section on exactly this. Although you can declare a type you can't actually create an object with it in TS:

    // No error on this type declaration:
    type Declarations = {
        [key: string]: string;
    } & {
        onMember: number;
        onCollection: number;
    }
    
    // Error does appear here indicating type `number` is not assignable to type `string`.
    const declarations: Declarations = {
        onMember: 0,
        onCollection: 0,
        other: 'Is a string'
    }
    

    TypeScript Playground link.

    0 讨论(0)
  • 2020-12-20 03:40

    There is no concrete type in TypeScript that represents your Declarations shape.

    I'd call the general idea a "default property" type. (GitHub issue asking for this is microsoft/TypeScript#17867) You want specific properties to be of one type, and then any others to "default" to some other incompatible type. It's like an index signature without the constraint that all properties must be assignable to it.

    ( Just to be clear, an index signature cannot be used:

    type BadDeclarations = {
        onMember: number, // error! number not assignable to string
        onCollection: number, // error! number not assignable to string
        [k: string]: string
    };
    

    The index signature [k: string]: string means every property must be assignable to string, even onMember and onCollection. To make an index signature that actually works, you'd need to widen the property type from string to string | number, which probably doesn't work for you. )

    There were some pull requests that would have made this possible, but it doesn't look like they are going to be part of the language any time soon.

    Often in TypeScript if there's no concrete type that works you can use a generic type which is constrained in some way. Here's how I'd make Declarations generic:

    type Declarations<T> = {
        [K in keyof T]: K extends 'onMember' | 'onCollection' ? number : string
    };
    

    And here's the signature for create()L

    function create<T extends Declarations<T>>(declarations: T) {
    }
    

    You can see that the declarations parameter is of type T, which is constrained to Declarations<T>. This self-referential constraint ensures that for every property K of declarations, it will be of type K extends 'onMember' | 'onCollection' ? number : string, a conditional type that is a fairly straightforward translation of your desired shape.

    Let's see if it works:

    create({
        onCollection: 1,
        onMember: 2,
        randomOtherThing: "hey"
    }); // okay
    
    create({
        onCollection: "oops", // error, string is not assignable to number
        onMember: 2,
        otherKey: "hey",
        somethingBad: 123, // error! number is not assignable to string
    })
    

    That looks reasonable to me.


    Of course, using a generic type isn't without some annoyances; suddenly every value or function that you wanted to use Declarations with will need to be generic now. So you can't do const foo: Declarations = {...}. You'd need const foo: Declarations<{onCollection: number, foo: string}> = {onCollection: 1, foo: ""} instead. That's obnoxious enough that you'd likely want to use a helper function like to allow such types to be inferred for you instead of manually annotated:

    // helper function
    const asDeclarations = <T extends Declarations<T>>(d: T): Declarations<T> => d;
    
    const foo = asDeclarations({ onCollection: 1, foo: "a" });
    /* const foo: Declarations<{
        onCollection: number;
        foo: string;
    }>*/
    

    Okay, hope that helps; good luck!

    Link to code

    0 讨论(0)
提交回复
热议问题