Search code examples
typescriptreturn-type

Typescript - exact return type of function


I have a problem with return type of the function. In the example, if I explicitly define return type Foo, it shows error as expected. If I add type to function, an error is not shown.

It seems that in foo2 should be at least object extended from Foo, if I remove "a" from foo2, it says this:

Type '() => { b: string; }' is not assignable to type 'FooFn'. Type '{ b: string; }' has no properties in common with type 'Foo'.

interface Foo {
    a?: string
}

type FooFn = () => Foo

const foo = (): Foo => {
    return {
        a: 'a',
        b: 'b', // OK
    }
}

const foo2: FooFn = () => {
    return {
        a: 'a',
        b: 'b', // Why is not error also here???
    }
}

The only option, that I think of is making it something like that:

interface Foo {
    a?: string
}

// I cant figure it out, how to write it ...
type FooFn = () => Record<string, key is from keyof Foo ? string : never>

Is it possible to make type, which only accepts keyof Foo and the rest is never?

Playground:

https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgGIHt3IN4FgBQyRycA-AFzIDOYUoA5gQL4EFgCeADihuqiMgC8yABQBKIQD40mVvgToQNZDExDRYyryk4CxZFAhgArlAF5C+4nEoByOLYA0eq0QBGdt0+QB6H8gB5AGkXIhZ8cIIFJTAVTAAmLUx+dXEdC31DEzNdS1cbZHsnUKsPQq9HX38AdQALdmRgKmQQdFjoKHQoEgAbKixa6AhSEZLwpiA


Solution

  • The behaviour you are observing for const foo2: FooFn = () => { is a result of TypeScript's Structural typing system which implies that as long as the returned value of foo2 has all the properties of Foo type - the function will pass no matter what other properties the resulting object may contain.

    The reason why you are seeing an error for const foo = (): Foo => { is because you are defining an explicit return type which acts like type annotation, whereas the use of FooFn kicks in a layer of abstraction that acts like type assertion which does not care about extra properties that are returned and only makes sure that the types have matching values.

    This can be illustrated by:

    let obj1: { 'a': string } = { a: 'a', b: 'b' } // will not pass for annotation
    let obj2 = { a: 'a', b: 'b' } as { 'a': string } // will pass for assertion
    

    Referring back to your example, using type assertion is safe as the returned object from foo2 will still have the type of Foo, meaning that only property a can be accessed no matter what else it has during application runtime.

    So unfortunately your problem is attempting to break TypeScript's type assertion convention and it may be worth reassessing your approach.

    Thankyou to @jcalz for helping to improve this answer.