ken
ken

Reputation: 8993

Type guard works for wrong type but accepts unknown type

I have the following "visual test" (aka, I'm just using vs-code to visually inspect whether there are or are not red squiggly lines under things). It's intent is to ensure that the generic type that I pass in does indeed constrain the called to substitute():

interface IJob {
  jobId: string;
  extraneous?: number;
}

/**
 * Bear in mind these tests are just to visually identify in
 * the editor that the generics are passing their types around
 * correctly.
 */
describe("Typings → ", () => {

  it.skip('setting <T> imposes structure on the substitutions that are allowed in', () => {
    const wrongType = {
      jobId: "1234",
      extraneous: "foobar"
    }
    const nonExistantType = {
      jobId: "1234",
      noCanDo: true
    }
    const ruleAbidingJob = {
      jobId: "1234"
    }
    const t = TypedTemplate.create<IJob>();
    t.substitute(wrongType); // this should be failing in TS
    t.substitute(nonExistantType); // this should be failing in TS
    t.substitute(ruleAbidingJob); // this should be fine
  });
});

It does seem to allow for the ruleAbidingJob and recognize correctly that wrongType has set extraneous to a string instead of a number as the interface defines it. Unfortunately it DOES allow nonExistantType to be used without error.

Here's the visual if that helps:

enter image description here

and also here's a screen shot showing that indeed the expected type for substitute is indeed IJob:

enter image description here

Upvotes: 0

Views: 106

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249506

Typescript considers the type containing the extra property (the type of nonExistantType) to be a subtype of IJob and this is why the call is allowed. You can consider the function parameter type to be the minimal set of properties the function needs to work, so if the passed in argument has more properties the function should not care.

Typescript does have a feature that marks extra properties as an error, but only when an object literal is directly assigned to a variable/argument of a specific type. In your case you can either pass the object literal directly to the function, or explicitly type the constant:

const nonExistantType :IJob = { // Error
    jobId: "1234",
    noCanDo: true
} 
    // OR
t.substitute({
    jobId: "1234",
    noCanDo: true // error
})

Edit

We can also check that there are no extra properties, post object creation, by using a conditional type in typescript 2.8:

const wrongType = {
    jobId: "1234",
    extraneous: "foobar"
}
const nonExistantType = {
    jobId: "1234",
    noCanDo: true
}
const ruleAbidingJob = {
    jobId: "1234"
}

type NoExtraProperties<TSource, TTarget> = Exclude<keyof TSource, keyof TTarget> extends never ? true : "Extra properties detected";
function ensueNothingExtra<TSource, TTarget>(source : TSource, validation: NoExtraProperties<TSource, TTarget>) :TTarget {
    return source as any
}
const t = TypedTemplate.create<IJob>();
t.substitute(ensueNothingExtra(wrongType, true)); // this is an error
t.substitute(ensueNothingExtra(nonExistantType, true)); // this is also an error
t.substitute(ensueNothingExtra(ruleAbidingJob, true)); // this is ok

Upvotes: 1

Related Questions