简体   繁体   中英

Why doesn't TypeScript complain about an incorrect return type?

Please consider this example:

// all properties in Item should be optional, this is by design
type Item = {
    id?: number
    name?: string
}

interface WithVersion {
    version: number
}

export type ResultType =
    & WithVersion // #1 try to remove it
    & {
        version: number
        list: Item[];
    };


export interface Data {
    list: Array<string>;
}

// As per my understanding, this function should not compile, because elem.list is not assignable to list in ResultType
const builder = <T extends Data>(data: Array<T>): ResultType[] => {
    const result = data.map((elem) => ({
        list: elem.list, // #2 list is string[], whereas ResultType expects list to be Item[]
        version: 2,
    }))
    return result // #3
}

Playground link

I am a bit confused by the assignability rules of TypeScript.

Try to remove WithVersion from ResultType . TypeScript will complain about the assignability of result to return type of builder function ( ResultType ).

Further more, if you define explicit return type for Array.prototype.map callback, TypeScript will complain just as I expect:

const builder = <T extends Data>(data: Array<T>): ResultType[] => {
    const result = data.map((elem):ResultType => ({
        list: elem.list, // error as expected
        version: 2,
    }))
    return result
}

My questions are:

  1. Why there is no error without explicit type for map callback. It is clear that string[] is not assignable to Item[] .

     declare let foo: string[] declare let bar: Item[] foo = bar bar = foo
  2. Why does an error appear when I remove WithVersion from the ResultType definition? It looks like that intersection of WithVersion and { version: number list: Item[]; }{ version: number list: Item[]; } somehow affects ResultType , whereas in my opinion, this intersection should not affect it at all.

SIMPLIFIED VERSION

type Item = {
    id?: number
    name?: string
}

interface WithVersion {
    version: number
}

export type ResultType =
    & WithVersion // #1 try to remove it
    & {
        version: number
        list: Item[];
    };

declare let result: ResultType;
declare let list: string[];
let a = {
    list,
    version: 2,
};
result = a;

Playground

I have created an issue in TS repo

TL;DR Weak type detection doesn't always occur for intersection types ; this seems to be behaving as designed, although the particular behavior you're seeing might be a design limitation.


From a purely structural standpoint, string is indeed assignable to Item . The Item type has optional properties id and name , and values of type string are missing these properties, so there's no apparent conflict. Reading "foo".id or "bar".name gives you undefined in both cases. If TypeScript only cared about structural compatibility, then none of your examples would have compiler errors.

Of course, it is probably a mistake to assign a string value to something that expects an Item . TypeScript has several features to try to catch these sort of non-type-safety mistakes. These are more like linter warnings than type errors.

One such feature is weak type detection . A weak type is an object type whose properties are all optional, like Item . Weak type detection causes the compiler to complain if you try to assign something to a weak type if there is no overlap in properties. (Aside: a more well-known such feature is excess property checking , in which object literals are not allowed to have unexpected properties.) Weak type detection is why you will get a warning if you assign a string to an Item or a string[] to an Item[] or an {x: string; y: string} {x: string; y: string} to an {x: Item; y: string} {x: Item; y: string} :

const foo = { x: "", y: "" }
const bar: { x: Item; y: string } = foo; // error

So then: why don't you get a warning when you try to assign an {x: string, y: string} to an {x: Item} & {y: string} ?

const baz: { x: Item } & { y: string } = foo; // no error?!

Why is there a difference with intersection types ?

Well, according to microsoft/TypeScript#16047 , the pull request that implemented weak type detection, weak type detection does not occur in intersection types unless all the intersected types are weak types. Since {y: string} is not a weak type (and technically neither is {x: Item} because the x property is not optional), then the intersection does not undergo weak type checking at all. Therefore things fall back to the normal structural type check, and the assignment succeeds.

This still raises the question of why weak type detection was implemented so that intersections are usually exempt. I don't really see this explicitly documented, but it looks like there was a prior version of this feature implemented at microsoft/TypeScript#3842 where weak type detection for intersections caused undesirable behavior. My presumption here is that the intent was to be somewhat conservative and only emit errors in cases known to be bad, and there are some intersection situations we want to accept (maybe generics?).


In any case, this is definitely behaving as designed. It might be a design limitation. I suppose we will wait to see what the official word is on microsoft/TypeScript#50608 before knowing for sure.

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM