Home > OS >  Pick<> don't work in function return type check?
Pick<> don't work in function return type check?

Time:10-09

interface TestInterface {
    id: number
    name: string
}

function tmp1(): Pick<TestInterface, "id"> {
    return {
        id: 123,
        name: "projectName", // Error: Type '{ id: number; name: string; }' is not assignable to type 'Pick<TestInterface, "id">'.
    }
}

function tmp2(): Pick<TestInterface, "id"> {
    const data = {
        id: 123,
        name: "projectName",
    }
    return data // No error
}

I have been confused for it for a long time...

Did I miss anything ?

If it works normally, can anyone explain it ?

Typescript version: 4.3.5

CodePudding user response:

This is a language feature, explained more detailed here: https://www.typescriptlang.org/docs/handbook/interfaces.html#excess-property-checks

Essentially, TypeScript tries to be smart. The object in the first example technically matches the interface, but it also knows you probably didn't mean to do that since you're defining an additional property that isn't needed.

In the second example, the object could have come from somewhere else, and since it matches the return type (despite having excess properties), it allows it so we don't have to go out our way to duplicate the object and remove the excess properties.

If you want to require the property not be there:

To remove a property:

Interface & { prop?: never }

To remove every property except one (what you want with Pick) is defined here with Exactly:

Why are excess properties allowed in Typescript when all properties in an interface are optional?

CodePudding user response:

Answer addenda

Adding to the solid answer from @DemiPixel; looking at the linked documentation:

Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the “target type” doesn’t have, you’ll get an error

which is reinforced by the full error text you received when running tsc:

test.ts:9:9 - error TS2322: Type '{ id: number; name: string; }' is not assignable to type 'Pick<TestInterface, "id">'.
  Object literal may only specify known properties, and 'name' does not exist in type 'Pick<TestInterface, "id">'.

9         name: "projectName",
          ~~~~~~~~~~~~~~~~~~~


Found 1 error.

Type systems in general

I also wanted to articulate my thoughts on type systems that came up when considering this question, and why this general permissiveness (notwithstanding the object literal case) makes sense to me.

My personal experience with types has shown them to generally define what capabilities an instance of that type has, but is not constrained to. So, reproducing the second function from your original example:

interface TestInterface {
    id: number
    name: string
}

function tmp2(): Pick<TestInterface, "id"> {
    const data = {
        id: 123,
        name: "projectName",
    }
    return data
}

at the point of invocation of that function, TypeScript will only hold that the returned value is at least capable of responding to access of the the id property (namely, of type { id: number }).

Illustrating this, if we have:

const result = tmp2()
console.log(result.id)
console.log(result.name)

the use of result.name will fail with:

test.ts:16:20 - error TS2339: Property 'name' does not exist on type 'Pick<TestInterface, "id">'.

16 console.log(result.name)
                      ~~~~


Found 1 error.

At some level, it makes sense to reach for the ability to constrain the value returned from inside the function, as this intuitively seems the safest option. But, as shown above, TypeScript effectively forgets that the value inside the function ever had any properties beyond those of the return type, and so any code that would attempt to access those properties will not be legal.

Transpiling and that pesky object literal

Moving on to the specific case of returning an object literal; my strongest suspicion is that this was put in place to partially address issues raised when TypeScript transpiled code is called from Javascript. One can imagine a scenario where your code is exported from a module, and imported and used like so:

import tmp2 from './test'

const result = tmp2()
console.log(result.id)
console.log(result.name)

As above, TypeScript will consider result to be { id: number } and give the same error. However, it is perfectly legal to import the same transpiled module (remember, this is just a Javascript module) from Javascript using the same syntax above, which, when executed, returns:

123
projectName

Thus, this excess property checking of object literals will nip this kind of use in the bud. In fact, if you think about it, object literals are the only source of instances that TypeScript can sensibly reason about and apply this kind of check on; any other instances will come from external sources at runtime, such as the network, storage or from the user. No static type check at transpile time will be able to protect against these containing excess properties.

Summary

So, based on both the theoretical end-to-end soundness of a type system and the slight hybrid model inherent to TypeScript transpilation, should it be necessary to restrict the definition of object literals to only those properties exactly expected by the target of assignment or use? It will depend on how likely you see parts of the transpiled output of your project used from a native Javascript context; I would posit that for the vast majority of TypeScript projects it would be TypeScript all the way down and hence not necessary.

  • Related